From 27b419c4462eddddc47135c506817d37667baf2f Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:04:26 +0200 Subject: [PATCH 001/187] test: Add basic authentication integration tests for SSE transport (#561) --- gradle/libs.versions.toml | 1 + integration-test/build.gradle.kts | 1 + .../integration/AbstractAuthenticationTest.kt | 188 ++++++++++++++++++ .../integration/sse/SseAuthenticationTest.kt | 41 ++++ 4 files changed, 231 insertions(+) create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/SseAuthenticationTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 08bf61eea..ccb649ddd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,6 +49,7 @@ ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" } ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-server-auth = { group = "io.ktor", name = "ktor-server-auth", version.ref = "ktor" } ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" } ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse", version.ref = "ktor" } ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" } diff --git a/integration-test/build.gradle.kts b/integration-test/build.gradle.kts index 82078e7c6..30afe9e8a 100644 --- a/integration-test/build.gradle.kts +++ b/integration-test/build.gradle.kts @@ -22,6 +22,7 @@ kotlin { implementation(libs.ktor.server.content.negotiation) implementation(libs.ktor.serialization) implementation(libs.ktor.server.websockets) + implementation(libs.ktor.server.auth) implementation(libs.ktor.server.test.host) implementation(libs.ktor.server.content.negotiation) implementation(libs.ktor.serialization) diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt new file mode 100644 index 000000000..1e88558fa --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt @@ -0,0 +1,188 @@ +package io.modelcontextprotocol.kotlin.sdk.integration + +import io.kotest.matchers.shouldBe +import io.ktor.client.HttpClient +import io.ktor.client.request.basicAuth +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.install +import io.ktor.server.auth.Authentication +import io.ktor.server.auth.UserIdPrincipal +import io.ktor.server.auth.authenticate +import io.ktor.server.auth.basic +import io.ktor.server.auth.principal +import io.ktor.server.engine.embeddedServer +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.routing.Route +import io.ktor.server.routing.routing +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import io.modelcontextprotocol.kotlin.test.utils.actualPort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds +import io.ktor.client.engine.cio.CIO as ClientCIO +import io.ktor.server.cio.CIO as ServerCIO +import io.ktor.server.sse.SSE as ServerSSE + +/** + * Base class for MCP authentication integration tests. + */ +@Suppress("InjectDispatcher") +abstract class AbstractAuthenticationTest { + + protected companion object { + const val HOST = "127.0.0.1" + const val AUTH_REALM = "mcp-auth" + const val WHOAMI_URI = "whoami://me" + const val VALID_USER = "test-user" + const val VALID_PASSWORD = "valid-password-123" + const val INVALID_USER = "invalid-user" + const val INVALID_PASSWORD = "invalid-password" + } + + /** + * Installs Ktor plugins required by the transport under test. + */ + protected open fun Application.configurePlugins() { + install(ServerSSE) + // ContentNegotiation is required by the StreamableHttp transport for JSON body handling. + // Installing it for SSE tests as well is harmless. + install(ContentNegotiation) { json(McpJson) } + } + + /** + * Registers the MCP server on the given route. + */ + abstract fun Route.registerMcpServer(serverFactory: ApplicationCall.() -> Server) + + /** + * Creates a client transport configured with the given credentials. + */ + abstract fun createClientTransport(baseUrl: String, user: String, pass: String): Transport + + @Test + fun `mcp behind basic auth rejects unauthenticated requests with 401`(): Unit = runBlocking(Dispatchers.IO) { + val server = startAuthenticatedServer() + + val httpClient = HttpClient(ClientCIO) { expectSuccess = false } + try { + httpClient.get("http://$HOST:${server.actualPort()}").status shouldBe HttpStatusCode.Unauthorized + } finally { + httpClient.close() + server.stopSuspend(1000, 2000) + } + } + + @Test + fun `mcp rejects requests with invalid credentials`(): Unit = runBlocking(Dispatchers.IO) { + val server = startAuthenticatedServer() + + val httpClient = HttpClient(ClientCIO) { + expectSuccess = false + } + try { + httpClient.get("http://$HOST:${server.actualPort()}") { + basicAuth(INVALID_USER, INVALID_PASSWORD) + }.status shouldBe HttpStatusCode.Unauthorized + } finally { + httpClient.close() + server.stopSuspend(1000, 2000) + } + } + + @Test + fun `authenticated mcp client can read resource scoped to principal`(): Unit = runBlocking(Dispatchers.IO) { + val server = startAuthenticatedServer() + + val baseUrl = "http://$HOST:${server.actualPort()}" + var mcpClient: Client? = null + try { + mcpClient = Client(Implementation(name = "test-client", version = "1.0.0")) + withTimeout(5.seconds) { + mcpClient.connect(createClientTransport(baseUrl, VALID_USER, VALID_PASSWORD)) + } + + val result = mcpClient.readResource( + ReadResourceRequest(ReadResourceRequestParams(uri = WHOAMI_URI)), + ) + + result.contents shouldBe listOf( + TextResourceContents( + text = VALID_USER, + uri = WHOAMI_URI, + mimeType = "text/plain", + ), + ) + } finally { + mcpClient?.close() + server.stopSuspend(1000, 2000) + } + } + + private suspend fun startAuthenticatedServer() = embeddedServer(ServerCIO, host = HOST, port = 0) { + configurePlugins() + installBasicAuth() + routing { + authenticate(AUTH_REALM) { + registerMcpServer { + createMcpServer { principal()?.name } + } + } + } + }.startSuspend(wait = false) + + private fun Application.installBasicAuth() { + install(Authentication) { + basic(AUTH_REALM) { + validate { credentials -> + if (credentials.name == VALID_USER && credentials.password == VALID_PASSWORD) { + UserIdPrincipal(credentials.name) + } else { + null + } + } + } + } + } + + protected fun createMcpServer(principalProvider: () -> String?): Server = Server( + serverInfo = Implementation(name = "test-server", version = "1.0.0"), + options = ServerOptions( + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(), + ), + ), + ).apply { + addResource( + uri = WHOAMI_URI, + name = "Current User", + description = "Returns the name of the authenticated user", + mimeType = "text/plain", + ) { + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = principalProvider() ?: "anonymous", + uri = WHOAMI_URI, + mimeType = "text/plain", + ), + ), + ) + } + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/SseAuthenticationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/SseAuthenticationTest.kt new file mode 100644 index 000000000..da7631951 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/sse/SseAuthenticationTest.kt @@ -0,0 +1,41 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.sse + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.basicAuth +import io.ktor.server.application.ApplicationCall +import io.ktor.server.routing.Route +import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport +import io.modelcontextprotocol.kotlin.sdk.integration.AbstractAuthenticationTest +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.mcp +import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import kotlin.test.AfterTest +import io.ktor.client.engine.cio.CIO as ClientCIO + +class SseAuthenticationTest : AbstractAuthenticationTest() { + + private var httpClient: HttpClient? = null + + @AfterTest + fun closeHttpClient() { + httpClient?.close() + httpClient = null + } + + override fun Route.registerMcpServer(serverFactory: ApplicationCall.() -> Server) { + mcp { + serverFactory(call) + } + } + + override fun createClientTransport(baseUrl: String, user: String, pass: String): Transport { + val client = HttpClient(ClientCIO) { install(SSE) } + httpClient = client + return SseClientTransport( + client = client, + urlString = baseUrl, + requestBuilder = { basicAuth(user, pass) }, + ) + } +} From 43b59c9fc1121002833d191aaaea8563538ae015 Mon Sep 17 00:00:00 2001 From: Jakub Amanowicz Date: Thu, 26 Feb 2026 12:00:46 +0100 Subject: [PATCH 002/187] fix: SseTransport endpoint resolution (#329) (#565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fix SSE endpoint resolution so absolute paths resolve against the origin, and add tests covering absolute vs relative endpoint events. ## Motivation and Context The SSE transport currently resolves all endpoint events against the SSE URL’s base path. When the server sends an absolute endpoint (starting with `/`), the client incorrectly prefixes it with the SSE path. This change resolves absolute endpoints against the origin while keeping relative behavior unchanged. addresses: https://github.com/modelcontextprotocol/kotlin-sdk/issues/329 ## How Has This Been Tested? Unit tests added in `SseClientTransportTest` for absolute and relative endpoint events. ## Breaking Changes No. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context The endpoint resolution logic now treats paths starting with `/` as origin-relative. Relative paths continue to resolve against the SSE base URL. --- .../kotlin/sdk/client/SseClientTransport.kt | 27 +++-- .../sdk/client/sse/MockSseClientEngine.kt | 99 ++++++++++++++++ .../sdk/client/sse/SseClientTransportTest.kt | 106 ++++++++++++++++++ 3 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt create mode 100644 kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt index 19480c7b5..3c9d88b86 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt @@ -11,7 +11,6 @@ import io.ktor.client.request.setBody import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.HttpHeaders -import io.ktor.http.Url import io.ktor.http.append import io.ktor.http.isSuccess import io.ktor.http.protocolWithAuthority @@ -54,13 +53,17 @@ public class SseClientTransport( private lateinit var scope: CoroutineScope private var job: Job? = null + private val origin: String by lazy { + session.call.request.url.protocolWithAuthority + } + private val baseUrl: String by lazy { session.call.request.url.let { url -> val path = url.encodedPath when { - path.isEmpty() -> url.protocolWithAuthority - path.endsWith("/") -> url.protocolWithAuthority + path.removeSuffix("/") - else -> url.protocolWithAuthority + path.take(path.lastIndexOf("/")) + path.isEmpty() -> origin + path.endsWith("/") -> origin + path.removeSuffix("/") + else -> origin + path.take(path.lastIndexOf("/")) } } } @@ -135,11 +138,21 @@ public class SseClientTransport( } } + /** + * Resolves and completes [endpoint] based on [eventData]. + * Uses full URLs as-is, treats absolute paths as origin-relative, + * and relative paths as relative to [baseUrl]. + */ private fun handleEndpoint(eventData: String) { try { - val path = if (eventData.startsWith("/")) eventData.substring(1) else eventData - val endpointUrl = Url("$baseUrl/$path") - endpoint.complete(endpointUrl.toString()) + val endpointUrl = if (eventData.startsWith("http://") || eventData.startsWith("https://")) { + eventData + } else if (eventData.startsWith("/")) { + origin + eventData + } else { + "$baseUrl/$eventData" + } + endpoint.complete(endpointUrl) logger.debug { "Client connected to endpoint: $endpointUrl" } } catch (e: Throwable) { _onError(e) diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt new file mode 100644 index 000000000..3627b6c3a --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt @@ -0,0 +1,99 @@ +package io.modelcontextprotocol.kotlin.sdk.client.sse + +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.HttpClientEngineConfig +import io.ktor.client.plugins.sse.SSECapability +import io.ktor.client.request.HttpRequestData +import io.ktor.client.request.HttpResponseData +import io.ktor.client.request.ResponseAdapterAttributeKey +import io.ktor.http.Headers +import io.ktor.http.HttpMethod +import io.ktor.http.HttpProtocolVersion +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.ktor.util.date.GMTDate +import io.ktor.utils.io.ByteChannel +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.InternalAPI +import io.ktor.utils.io.writeStringUtf8 +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlin.coroutines.CoroutineContext + +@OptIn(InternalAPI::class) +internal open class MockSseClientEngine( + private val endpoint: String, + private val onPostRequest: (postRequest: HttpRequestData) -> Unit, + override val dispatcher: CoroutineDispatcher = Dispatchers.Default, +) : HttpClientEngine { + + override val config = HttpClientEngineConfig() + override val supportedCapabilities = setOf(SSECapability) + + private val job = SupervisorJob() + override val coroutineContext: CoroutineContext = dispatcher + job + + private val channel by lazy { ByteChannel(true) } + + override suspend fun execute(data: HttpRequestData): HttpResponseData = when (data.method) { + HttpMethod.Get -> respondWithSseEndpointEvent(data) + + HttpMethod.Post -> { + onPostRequest(data) + respondWithAccepted() + } + + else -> error("Unsupported method: ${data.method}") + } + + override fun close() { + channel.close() + job.cancel() + } + + private fun responseContext(): CoroutineContext = dispatcher + Job() + + private suspend fun respondWithSseEndpointEvent(data: HttpRequestData): HttpResponseData { + val sseResponseAdapter = requireNotNull(data.attributes.getOrNull(ResponseAdapterAttributeKey)) { + "Missing SSE response adapter." + } + channel.writeStringUtf8(endpointEvent()) + channel.flush() + val responseContext = responseContext() + val responseBody = sseResponseAdapter.adapt( + data, + HttpStatusCode.OK, + eventStreamHeader, + channel, + data.body, + responseContext, + ) ?: error("Failed to adapt SSE response.") + return HttpResponseData( + statusCode = HttpStatusCode.OK, + requestTime = GMTDate(), + headers = eventStreamHeader, + version = HttpProtocolVersion.HTTP_1_1, + body = responseBody, + callContext = responseContext, + ) + } + + private val eventStreamHeader by lazy { headersOf("Content-Type", "text/event-stream") } + + private fun endpointEvent(): String = buildString { + appendLine("event: endpoint") + appendLine("data: $endpoint") + appendLine() + } + + private fun respondWithAccepted(): HttpResponseData = HttpResponseData( + statusCode = HttpStatusCode.Accepted, + requestTime = GMTDate(), + headers = Headers.Empty, + version = HttpProtocolVersion.HTTP_1_1, + body = ByteReadChannel.Empty, + callContext = responseContext(), + ) +} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt new file mode 100644 index 000000000..6cc7f6e93 --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt @@ -0,0 +1,106 @@ +package io.modelcontextprotocol.kotlin.sdk.client.sse + +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.HttpRequestData +import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class SseClientTransportTest { + + @Test + fun `absolute path endpoint resolves against origin`() = runTest { + // Given + val sseUrl = "http://example.com/api/mcp/sse" + + // And + val endpointEvent = "/messages?sessionId=abc" + + // And + val engine = CapturingSseClientEngine(endpoint = endpointEvent) + val transport = sseTransport(sseUrl, engine) + + // When + transport.start() + transport.send(JSONRPCNotification(method = "test")) + + // Then + val capturedPosts = engine.capturedPosts + capturedPosts shouldHaveSize 1 + capturedPosts[0].url.toString() shouldBe "http://example.com/messages?sessionId=abc" + + // Cleanup + transport.close() + engine.close() + } + + @Test + fun `relative path endpoint resolves against baseUrl`() = runTest { + // Given + val sseUrl = "http://example.com/api/mcp/sse" + + // And + val endpointEvent = "post?sessionId=xyz" + + // And + val engine = CapturingSseClientEngine(endpoint = endpointEvent) + val transport = sseTransport(sseUrl, engine) + + // When + transport.start() + transport.send(JSONRPCNotification(method = "test")) + + // Then + val capturedPosts = engine.capturedPosts + capturedPosts shouldHaveSize 1 + capturedPosts[0].url.toString() shouldBe "http://example.com/api/mcp/post?sessionId=xyz" + + // Cleanup + transport.close() + engine.close() + } + + @Test + fun `full url endpoint is used as-is`() = runTest { + // Given + val sseUrl = "http://example.com/api/mcp/sse" + + // And + val endpointEvent = "https://example.com/messages?sessionId=abc" + + // And + val engine = CapturingSseClientEngine(endpoint = endpointEvent) + val transport = sseTransport(sseUrl, engine) + + // When + transport.start() + transport.send(JSONRPCNotification(method = "test")) + + // Then + val capturedPosts = engine.capturedPosts + capturedPosts shouldHaveSize 1 + capturedPosts[0].url.toString() shouldBe "https://example.com/messages?sessionId=abc" + + // Cleanup + transport.close() + engine.close() + } + + private fun sseTransport(sseUrl: String, engine: CapturingSseClientEngine) = + SseClientTransport(HttpClient(engine) { install(SSE) }, sseUrl) + + private class CapturingSseClientEngine private constructor( + endpoint: String, + private val capturedPostRequests: MutableList = mutableListOf(), + ) : MockSseClientEngine(endpoint, capturedPostRequests::add) { + + constructor(endpoint: String) : this(endpoint, mutableListOf()) + + val capturedPosts: List + get() = capturedPostRequests + } +} From ec53adc317ce13af54ab4d43ce7d16e826d0c268 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:21:49 +0200 Subject: [PATCH 003/187] feat: introduce StreamableHttpServerTransport.Configuration (#560) ## Introduce Configuration class for StreamableHttpServerTransport Replace the six-parameter flat constructor of `StreamableHttpServerTransport` with a typed `Configuration` class. This improves API ergonomics, enables structural equality and copy(), and provides a stable extension point for future options without further breaking the constructor signature. ### Changes: - Add `Configuration` as a public class nested directly on `StreamableHttpServerTransport`, with `enableJsonResponse` as the first parameter (most commonly set) - Change the primary constructor to accept `Configuration`. - Rename `retryIntervalMillis: Long?` to `retryInterval: Duration?` in Configuration, aligning with Kotlin's type-safe time API - Deprecate the old flat constructor with a compatibility bridge - Update KotlinTestBase integration test to use the new constructor - Enable test AbstractResourceIntegrationTest.testSubscribeAndUnsubscribe() since #249 is closed ## Motivation and Context The current StreamableHttpServerTransport API cannot be easily extended: adding more parameters would be a breaking change. But this is already needed for #521. This PR is a prerequisite for #535 ## How Has This Been Tested? Regression tests ## Breaking Changes No. Current StreamableHttpServerTransport constructor was deprecated ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- .../kotlin/AbstractResourceIntegrationTest.kt | 2 - .../sdk/integration/kotlin/KotlinTestBase.kt | 4 +- kotlin-sdk-server/api/kotlin-sdk-server.api | 12 ++ .../kotlin/sdk/server/KtorServer.kt | 24 +-- .../server/StreamableHttpServerTransport.kt | 137 +++++++++++++----- 5 files changed, 128 insertions(+), 51 deletions(-) diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt index fc0881036..40b955204 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.util.concurrent.atomic.AtomicBoolean -import kotlin.test.Ignore import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -165,7 +164,6 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { assertEquals(testResourceContent, content.text, "Resource content should match") } - @Ignore("Blocked by https://github.com/modelcontextprotocol/kotlin-sdk/issues/249") @Test fun testSubscribeAndUnsubscribe() { runBlocking(Dispatchers.IO) { diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt index f8e8740e2..2f8b3872f 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt @@ -148,7 +148,9 @@ abstract class KotlinTestBase { // Create StreamableHTTP server transport // Using JSON response mode for simpler testing (no SSE session required) val transport = StreamableHttpServerTransport( - enableJsonResponse = true, // Use JSON response mode for testing + StreamableHttpServerTransport.Configuration( + enableJsonResponse = true, // Use JSON response mode for testing + ), ) // Use stateless mode to skip session validation for simpler testing transport.setSessionIdGenerator(null) diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index 15bbe5ee1..c0b0199dd 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -195,6 +195,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTranspor public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { public static final field STANDALONE_SSE_STREAM_ID Ljava/lang/String; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration;)V public fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Ljava/lang/Long;)V public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -211,6 +212,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServe public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration { + public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAllowedHosts ()Ljava/util/List; + public final fun getAllowedOrigins ()Ljava/util/List; + public final fun getEnableDnsRebindingProtection ()Z + public final fun getEnableJsonResponse ()Z + public final fun getEventStore ()Lio/modelcontextprotocol/kotlin/sdk/server/EventStore; + public final fun getRetryInterval-FghU774 ()Lkotlin/time/Duration; +} + public final class io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensionsKt { public static final fun mcpWebSocket (Lio/ktor/server/application/Application;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V public static final fun mcpWebSocket (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function0;)V 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 5a750a5d6..d3cbf4019 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 @@ -225,11 +225,13 @@ private suspend fun RoutingContext.mcpStatelessStreamableHttpEndpoint( block: RoutingContext.() -> Server, ) { val transport = StreamableHttpServerTransport( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, - enableJsonResponse = true, + StreamableHttpServerTransport.Configuration( + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + eventStore = eventStore, + enableJsonResponse = true, + ), ).also { it.setSessionIdGenerator(null) } logger.info { "New stateless StreamableHttp connection established without sessionId" } @@ -305,11 +307,13 @@ private suspend fun RoutingContext.streamableTransport( } val transport = StreamableHttpServerTransport( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, - enableJsonResponse = true, + StreamableHttpServerTransport.Configuration( + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + eventStore = eventStore, + enableJsonResponse = true, + ), ) transport.setOnSessionInitialized { initializedSessionId -> diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 1894cdc45..580fcc0e5 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -36,6 +36,8 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.decodeFromJsonElement import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -46,8 +48,8 @@ private const val MAXIMUM_MESSAGE_SIZE = 4 * 1024 * 1024 // 4 MB /** * A holder for an active request call. - * If enableJsonResponse is true, session is null. - * Otherwise, session is not null. + * If [StreamableHttpServerTransport.Configuration.enableJsonResponse] is true, the session is null. + * Otherwise, the session is not null. */ private data class SessionContext(val session: ServerSSESession?, val call: ApplicationCall) @@ -66,32 +68,87 @@ private data class SessionContext(val session: ServerSSESession?, val call: Appl * - No Session ID is included in any responses * - No session validation is performed * - * @param enableJsonResponse If true, the server will return JSON responses instead of starting an SSE stream. - * This can be useful for simple request/response scenarios without streaming. - * Default is false (SSE streams are preferred). - * @param enableDnsRebindingProtection Enable DNS rebinding protection - * (requires allowedHosts and/or allowedOrigins to be configured). - * Default is false for backwards compatibility. - * @param allowedHosts List of allowed host header values for DNS rebinding protection. - * If not specified, host validation is disabled. - * @param allowedOrigins List of allowed origin header values for DNS rebinding protection. - * If not specified, origin validation is disabled. - * @param eventStore Event store for resumability support - * If provided, resumability will be enabled, allowing clients to reconnect and resume messages - * @param retryIntervalMillis Retry interval (in milliseconds) advertised via SSE priming events - * to hint the client when to reconnect. Applies only when an [eventStore] is configured. - * Defaults to `null` (no retry hint). + * @param configuration Transport configuration. See [Configuration] for available options. */ @OptIn(ExperimentalUuidApi::class, ExperimentalAtomicApi::class) @Suppress("TooManyFunctions") -public class StreamableHttpServerTransport( - private val enableJsonResponse: Boolean = false, - private val enableDnsRebindingProtection: Boolean = false, - private val allowedHosts: List? = null, - private val allowedOrigins: List? = null, - private val eventStore: EventStore? = null, - private val retryIntervalMillis: Long? = null, -) : AbstractTransport() { +public class StreamableHttpServerTransport(private val configuration: Configuration) : AbstractTransport() { + + @Deprecated("Use default constructor with explicit Configuration()") + public constructor() : this(configuration = Configuration()) + + /** + * Secondary constructor for `StreamableHttpServerTransport` that simplifies initialization by directly taking the + * configurable parameters without requiring a `Configuration` instance. + * + * @param enableJsonResponse Determines whether the server should return JSON responses. + * Defaults to `false`. + * @param enableDnsRebindingProtection Enables DNS rebinding protection. + * Defaults to `false`. + * @param allowedHosts A list of hosts allowed for server communication. + * Defaults to `null`, allowing all hosts. + * @param allowedOrigins A list of allowed origins for CORS (Cross-Origin Resource Sharing). + * Defaults to `null`, allowing all origins. + * @param eventStore The `EventStore` instance for handling resumable events. + * Defaults to `null`, disabling resumability. + * @param retryIntervalMillis Retry interval in milliseconds for event handling or reconnection attempts. + * Defaults to `null`. + */ + @Suppress("MaxLineLength") + @Deprecated( + "Use constructor with Configuration: StreamableHttpServerTransport(Configuration(enableJsonResponse = ...))", + replaceWith = ReplaceWith( + "StreamableHttpServerTransport(Configuration(enableJsonResponse = enableJsonResponse, enableDnsRebindingProtection = enableDnsRebindingProtection, allowedHosts = allowedHosts, allowedOrigins = allowedOrigins, eventStore = eventStore, retryIntervalMillis = retryIntervalMillis))", + ), + ) + public constructor( + enableJsonResponse: Boolean = false, + enableDnsRebindingProtection: Boolean = false, + allowedHosts: List? = null, + allowedOrigins: List? = null, + eventStore: EventStore? = null, + retryIntervalMillis: Long? = null, + ) : this( + Configuration( + enableJsonResponse = enableJsonResponse, + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + eventStore = eventStore, + retryInterval = retryIntervalMillis?.milliseconds, + ), + ) + + /** + * Configuration for managing various aspects of the StreamableHttpServerTransport. + * + * @property enableJsonResponse Determines whether the server should return JSON responses. + * Defaults to `false`. + * + * @property enableDnsRebindingProtection Enables DNS rebinding protection. + * Defaults to `false`. + * + * @property allowedHosts A list of hosts allowed for server communication. + * Defaults to `null`, allowing all hosts. + * + * @property allowedOrigins A list of allowed origins for CORS (Cross-Origin Resource Sharing). + * Defaults to `null`, allowing all origins. + * + * @property eventStore The `EventStore` instance for handling resumable events. + * Defaults to `null`, disabling resumability. + * + * @property retryInterval Retry interval for event handling or reconnection attempts. + * Defaults to `null`. + */ + public class Configuration( + public val enableJsonResponse: Boolean = false, + public val enableDnsRebindingProtection: Boolean = false, + public val allowedHosts: List? = null, + public val allowedOrigins: List? = null, + public val eventStore: EventStore? = null, + public val retryInterval: Duration? = null, + ) + public var sessionId: String? = null private set @@ -177,7 +234,7 @@ public class StreamableHttpServerTransport( ?: error("No connection established for request id $routingRequestId") val activeStream = streamsMapping[streamId] - if (!enableJsonResponse) { + if (!configuration.enableJsonResponse) { activeStream?.let { stream -> emitOnStream(streamId, stream.session, message) } @@ -194,7 +251,7 @@ public class StreamableHttpServerTransport( streamMutex.withLock { if (activeStream == null) error("No connection established for request ID: $routingRequestId") - if (enableJsonResponse) { + if (configuration.enableJsonResponse) { activeStream.call.response.header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) sessionId?.let { activeStream.call.response.header(MCP_SESSION_ID_HEADER, it) } val responses = relatedIds.mapNotNull { requestToResponseMapping[it] } @@ -261,7 +318,7 @@ public class StreamableHttpServerTransport( @Suppress("CyclomaticComplexMethod", "LongMethod", "ReturnCount", "TooGenericExceptionCaught") public suspend fun handlePostRequest(session: ServerSSESession?, call: ApplicationCall) { try { - if (!enableJsonResponse && session == null) { + if (!configuration.enableJsonResponse && session == null) { error("Server session can't be null for SSE responses") } @@ -328,7 +385,7 @@ public class StreamableHttpServerTransport( } val streamId = Uuid.random().toString() - if (!enableJsonResponse) { + if (!configuration.enableJsonResponse) { call.appendSseHeaders() flushSse(session) // flush headers immediately maybeSendPrimingEvent(streamId, session) @@ -353,7 +410,7 @@ public class StreamableHttpServerTransport( @Suppress("ReturnCount") public suspend fun handleGetRequest(session: ServerSSESession?, call: ApplicationCall) { - if (enableJsonResponse) { + if (configuration.enableJsonResponse) { call.reject( HttpStatusCode.MethodNotAllowed, RPCError.ErrorCode.CONNECTION_CLOSED, @@ -375,7 +432,7 @@ public class StreamableHttpServerTransport( if (!validateSession(call) || !validateProtocolVersion(call)) return - eventStore?.let { store -> + configuration.eventStore?.let { store -> call.request.header(MCP_RESUMPTION_TOKEN_HEADER)?.let { lastEventId -> replayEvents(store, lastEventId, sseSession) return @@ -413,7 +470,7 @@ public class StreamableHttpServerTransport( */ @Suppress("ReturnCount", "TooGenericExceptionCaught") public suspend fun closeSseStream(requestId: RequestId) { - if (enableJsonResponse) return + if (configuration.enableJsonResponse) return val streamId = requestToStreamMapping[requestId] ?: return val sessionContext = streamsMapping[streamId] ?: return @@ -562,9 +619,9 @@ public class StreamableHttpServerTransport( @Suppress("ReturnCount") private fun validateHeaders(call: ApplicationCall): String? { - if (!enableDnsRebindingProtection) return null + if (!configuration.enableDnsRebindingProtection) return null - allowedHosts?.let { hosts -> + configuration.allowedHosts?.let { hosts -> val hostHeader = call.request.headers[HttpHeaders.Host]?.lowercase() val allowedHostsLowercase = hosts.map { it.lowercase() } @@ -573,7 +630,7 @@ public class StreamableHttpServerTransport( } } - allowedOrigins?.let { origins -> + configuration.allowedOrigins?.let { origins -> val originHeader = call.request.headers[HttpHeaders.Origin]?.lowercase() val allowedOriginsLowercase = origins.map { it.lowercase() } @@ -636,7 +693,7 @@ public class StreamableHttpServerTransport( this?.lowercase()?.contains(mime.toString().lowercase()) == true private suspend fun emitOnStream(streamId: String, session: ServerSSESession?, message: JSONRPCMessage) { - val eventId = eventStore?.storeEvent(streamId, message) + val eventId = configuration.eventStore?.storeEvent(streamId, message) try { session?.send(event = "message", id = eventId, data = McpJson.encodeToString(message)) } catch (_: Exception) { @@ -646,11 +703,15 @@ public class StreamableHttpServerTransport( @Suppress("TooGenericExceptionCaught") private suspend fun maybeSendPrimingEvent(streamId: String, session: ServerSSESession?) { - val store = eventStore ?: return + val store = configuration.eventStore ?: return val sseSession = session ?: return try { val primingEventId = store.storeEvent(streamId, JSONRPCEmptyMessage) - sseSession.send(id = primingEventId, retry = retryIntervalMillis, data = "") + sseSession.send( + id = primingEventId, + retry = configuration.retryInterval?.inWholeMilliseconds, + data = "", + ) } catch (e: Exception) { _onError(e) } From 8a9922be793aa7086bcfce4c1ae022309990cf70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:11:14 +0000 Subject: [PATCH 004/187] chore(deps): bump actions/upload-artifact from 6 to 7 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 6 +++--- .github/workflows/conformance.yml | 2 +- .github/workflows/samples.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 00411744d..8ab52f4fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -88,7 +88,7 @@ jobs: - name: Upload Reports if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: reports-build-${{ matrix.os }} path: | @@ -114,7 +114,7 @@ jobs: - name: Upload Maven Local Repository if: ${{ success() && matrix.os == 'ubuntu-latest' }} - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: local-maven-repository path: local-maven-repository.zip @@ -206,7 +206,7 @@ jobs: - name: Upload Reports if: ${{ !cancelled() }} - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: reports-samples-snapshot-${{ matrix.sample }} path: | diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 2c7b1ef23..1c9ba19ca 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -67,7 +67,7 @@ jobs: - name: Upload Conformance Results if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: conformance-results-${{ matrix.os }} path: conformance-test/results/ diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 7fac3881a..cdd98bb08 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -57,7 +57,7 @@ jobs: - name: Upload Reports if: ${{ !cancelled() }} - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: reports-samples-${{ matrix.sample }} path: | From e124262982a61e0f28dc70c5103c967d5b0ab8f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:11:17 +0000 Subject: [PATCH 005/187] chore(deps): bump actions/download-artifact from 7 to 8 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ab52f4fb..bb28aafc4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -190,7 +190,7 @@ jobs: ../.konan/** - name: Download Maven Local Repository - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: local-maven-repository path: . From 45fcc17a23b5a1984e602c4289f9e177d5f40ec8 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:47:05 +0200 Subject: [PATCH 006/187] doc: update API reference URL in README.md (#578) Update link in readme.md ## Motivation and Context Kotlin SDK has moved from https://modelcontextprotocol.github.io/kotlin-sdk/ to https://kotlin.sdk.modelcontextprotocol.io/ - [Announcement](https://discord.com/channels/1358869848138059966/1399986970851282954/1476078438266830931) - DNS change PR: https://github.com/modelcontextprotocol/dns/pull/5 ## Breaking Changes No ## Types of changes - [x] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [ ] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [ ] I have added appropriate error handling - [x] I have added or updated documentation as needed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 50beb545a..505a592c5 100644 --- a/README.md +++ b/README.md @@ -887,7 +887,7 @@ allowing for easy testing of MCP functionality without the need for network setu ## Documentation -- [API Reference](https://modelcontextprotocol.github.io/kotlin-sdk/) +- [API Reference](https://kotlin.sdk.modelcontextprotocol.io/) - [Model Context Protocol documentation](https://modelcontextprotocol.io) - [MCP specification](https://modelcontextprotocol.io/specification/latest) From c36881282f977e7469115b7b980987b17c4f4aa5 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:19:53 +0200 Subject: [PATCH 007/187] ci: add Codecov coverage upload and XML report generation (#579) ## Add Codecov coverage upload and XML report generation - Add `koverXmlReport` to Gradle tasks in Ubuntu workflow - Configure Codecov action to upload coverage reports Reports will be available on https://app.codecov.io/gh/modelcontextprotocol/kotlin-sdk ## Motivation and Context To provide better test coverage visibility ## How Has This Been Tested? CI ## Breaking Changes No ## Types of changes - [x] CI update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context --- .github/workflows/build.yml | 20 ++++++++++++++- README.md | 2 ++ codecov.yml | 51 +++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 codecov.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb28aafc4..d41454c7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: include: - os: ubuntu-latest job-name: "Linux Tests" - gradle-tasks: "build ktlintCheck koverLog koverHtmlReport publishToMavenLocal -Pversion=1-SNAPSHOT" + gradle-tasks: "build ktlintCheck koverLog koverHtmlReport koverXmlReport publishToMavenLocal -Pversion=1-SNAPSHOT" max-workers: 3 - os: windows-latest @@ -107,6 +107,24 @@ jobs: report_paths: '**/test-results/**/TEST-*.xml' truncate_stack_traces: false + - name: Upload coverage reports to Codecov + if: matrix.os == 'ubuntu-latest' + uses: codecov/codecov-action@v5 + with: + directory: ./build/reports/kover + files: report.xml + + - name: Upload coverage reports to Codecov + if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} + uses: codecov/codecov-action@v5 + with: + directory: ./build/reports/kover + files: report.xml + + - name: Upload test results to Codecov + if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} + uses: codecov/test-results-action@v1 + - name: Archive Maven Local Repository if: ${{ success() && matrix.os == 'ubuntu-latest' }} run: | diff --git a/README.md b/README.md index 505a592c5..30d1b40d3 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![Maven Central](https://img.shields.io/maven-central/v/io.modelcontextprotocol/kotlin-sdk.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/io.modelcontextprotocol/kotlin-sdk) [![Build](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/build.yml) [![Conformance Tests](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/conformance.yml/badge.svg)](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/conformance.yml) +[![codecov](https://codecov.io/github/modelcontextprotocol/kotlin-sdk/graph/badge.svg?token=O2CFHNSFYM)](https://codecov.io/github/modelcontextprotocol/kotlin-sdk) + [![Kotlin](https://img.shields.io/badge/kotlin-2.2+-blueviolet.svg?logo=kotlin)](http://kotlinlang.org) [![Kotlin Multiplatform](https://img.shields.io/badge/Platforms-%20JVM%20%7C%20Wasm%2FJS%20%7C%20Native%20-blueviolet?logo=kotlin)](https://kotlinlang.org/docs/multiplatform.html) [![JVM](https://img.shields.io/badge/JVM-11+-red.svg?logo=jvm)](http://java.com) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..9135aae1e --- /dev/null +++ b/codecov.yml @@ -0,0 +1,51 @@ +# Codecov configuration for Mokksy and AI-Mocks +# https://docs.codecov.com/docs/codecov-yaml + +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + status: + project: + default: + target: auto + threshold: 1% + patch: + default: + target: auto + threshold: 1% + +comment: + layout: "header, diff, flags, components" + behavior: default + require_changes: true + +component_management: + default_rules: + statuses: + - type: project + target: auto + individual_components: + + - component_id: kotlin-sdk-core + name: SDK Core + paths: + - kotlin-sdk-core/** + + - component_id: kotlin-sdk-client + name: SDK Client + paths: + - kotlin-sdk-client/** + + - component_id: kotlin-sdk-server + name: SDK Server + paths: + - kotlin-sdk-server/** + + - component_id: kotlin-sdk-testing + name: SDK Testing + paths: + - kotlin-sdk-testing/** + From eb94f1413254208aa2a96e37b2de93d10a775826 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:41:14 +0200 Subject: [PATCH 008/187] chore(deps): bump dev.mokksy:mokksy from 0.6.2 to 0.8.0 (#557) Bumps [dev.mokksy:mokksy](https://github.com/mokksy/ai-mocks) from 0.6.2 to 0.8.0. - Update dependency and migrate to new API --------- Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- .../AbstractStreamableHttpClientTest.kt | 9 +++++- .../kotlin/sdk/client/MockMcp.kt | 30 ++++++++++++++----- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ccb649ddd..f86685d37 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ kotlinx-io = "0.8.2" ktor = "3.2.3" logging = "7.0.14" mockk = "1.14.9" -mokksy = "0.6.2" +mokksy = "0.8.0" serialization = "1.9.0" slf4j = "2.0.17" junit = "5.14.2" diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt index 77ba553e4..4a04e8163 100644 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt @@ -5,6 +5,7 @@ import io.ktor.client.engine.apache5.Apache5 import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.sse.SSE +import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.TestInstance @@ -16,7 +17,13 @@ internal abstract class AbstractStreamableHttpClientTest { @AfterEach fun afterEach() { - mockMcp.checkForUnmatchedRequests() + mockMcp.verifyNoUnexpectedRequests() + } + + @AfterAll + fun afterAll() { + mockMcp.verifyNoUnmatchedStubs() + mockMcp.close() } protected suspend fun connect(client: Client) { diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt index ff3997392..fb719041c 100644 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt @@ -3,6 +3,8 @@ package io.modelcontextprotocol.kotlin.sdk.client import dev.mokksy.mokksy.BuildingStep import dev.mokksy.mokksy.Mokksy import dev.mokksy.mokksy.StubConfiguration +import dev.mokksy.mokksy.shutdown +import dev.mokksy.mokksy.start import io.ktor.http.ContentType import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode @@ -21,16 +23,23 @@ import kotlinx.serialization.json.putJsonObject const val MCP_SESSION_ID_HEADER = "MCP-Session-Id" -internal class MockMcp(verbose: Boolean = false) { +internal class MockMcp(verbose: Boolean = false) : AutoCloseable { - private val mokksy: Mokksy = Mokksy(verbose = verbose) + private val mokksy: Mokksy = Mokksy(verbose = verbose).apply { + start() + } + + fun verifyNoUnexpectedRequests() { + mokksy.verifyNoUnexpectedRequests() + } - fun checkForUnmatchedRequests() { - mokksy.checkForUnmatchedRequests() + fun verifyNoUnmatchedStubs() { + mokksy.verifyNoUnmatchedStubs() } val url = "${mokksy.baseUrl()}/mcp" + @Suppress("LongParameterList") fun onInitialize( clientName: String? = null, sessionId: String, @@ -108,6 +117,7 @@ internal class MockMcp(verbose: Boolean = false) { } } + @Suppress("LongParameterList") fun handleWithResult( httpMethod: HttpMethod = HttpMethod.Post, jsonRpcMethod: String, @@ -124,9 +134,9 @@ internal class MockMcp(verbose: Boolean = false) { expectedSessionId = expectedSessionId, bodyPredicates = bodyPredicates, ) respondsWith { - val requestId = when (request.body.id) { - is RequestId.NumberId -> (request.body.id as RequestId.NumberId).value.toString() - is RequestId.StringId -> "\"${(request.body.id as RequestId.StringId).value}\"" + val requestId = when (val id = request.body().id) { + is RequestId.NumberId -> id.value.toString() + is RequestId.StringId -> "\"${id.value}\"" } val resultObject = result.invoke() // language=json @@ -143,6 +153,7 @@ internal class MockMcp(verbose: Boolean = false) { } } + @Suppress("LongParameterList") fun handleWithResult( httpMethod: HttpMethod = HttpMethod.Post, jsonRpcMethod: String, @@ -167,6 +178,7 @@ internal class MockMcp(verbose: Boolean = false) { ) } + @Suppress("LongParameterList") fun handleJSONRPCRequest( httpMethod: HttpMethod = HttpMethod.Post, jsonRpcMethod: String, @@ -222,4 +234,8 @@ internal class MockMcp(verbose: Boolean = false) { body = null } } + + override fun close() { + mokksy.shutdown() + } } From 66d31d32a90d377a832dfc8a0343fabc4a723efd Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:00:11 +0200 Subject: [PATCH 009/187] ci: update Codecov configuration for test and coverage upload (#580) ci: update Codecov configuration for test and coverage upload - Unified usage of `codecov/codecov-action@v5` for both test results and coverage upload. - Added `CODECOV_TOKEN` and `report_type` parameters for improved configuration. ## Motivation and Context Actually, CODECOV_TOKEN is required, even though the CodeCov banner previously said the project can upload without a token. ## How Has This Been Tested? CI ## Breaking Changes No ## Types of changes - [x] CI fix ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- .github/workflows/build.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d41454c7d..2b769d109 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -108,22 +108,21 @@ jobs: truncate_stack_traces: false - name: Upload coverage reports to Codecov - if: matrix.os == 'ubuntu-latest' + if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} uses: codecov/codecov-action@v5 with: + token: ${{ secrets.CODECOV_TOKEN }} + report_type: coverage directory: ./build/reports/kover files: report.xml - - name: Upload coverage reports to Codecov + - name: Upload test results to Codecov if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} uses: codecov/codecov-action@v5 with: - directory: ./build/reports/kover - files: report.xml + token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results - - name: Upload test results to Codecov - if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} - uses: codecov/test-results-action@v1 - name: Archive Maven Local Repository if: ${{ success() && matrix.os == 'ubuntu-latest' }} From 9eabb54cb5bef458bb3161ada0c616c4118265b9 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:51:37 +0200 Subject: [PATCH 010/187] refactor: refactor Streamable HTTP ktor extensions (#562) (#568) --- .../kotlin/sdk/server/KtorServer.kt | 202 +++++++++++------- .../kotlin/sdk/server/TransportManager.kt | 22 ++ 2 files changed, 142 insertions(+), 82 deletions(-) create mode 100644 kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/TransportManager.kt 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 d3cbf4019..739f8d3c1 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 @@ -1,3 +1,5 @@ +@file:Suppress("TooManyFunctions") + package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging @@ -20,38 +22,22 @@ import io.ktor.server.sse.SSE import io.ktor.server.sse.ServerSSESession import io.ktor.server.sse.sse import io.ktor.utils.io.KtorDsl -import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.types.RPCError -import kotlinx.atomicfu.AtomicRef -import kotlinx.atomicfu.atomic -import kotlinx.atomicfu.update -import kotlinx.collections.immutable.PersistentMap -import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.awaitCancellation private val logger = KotlinLogging.logger {} -internal class TransportManager(transports: Map = emptyMap()) { - private val transports: AtomicRef> = atomic(transports.toPersistentMap()) - - fun hasTransport(sessionId: String): Boolean = transports.value.containsKey(sessionId) - - fun getTransport(sessionId: String): AbstractTransport? = transports.value[sessionId] - - fun addTransport(sessionId: String, transport: AbstractTransport) { - transports.update { it.put(sessionId, transport) } - } - - fun removeTransport(sessionId: String) { - transports.update { it.remove(sessionId) } - } -} - /** - * Registers a server-sent events (SSE) route at the specified path. + * Registers MCP over [Server-Sent Events (SSE) Transport](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) + * at the specified [path] on this [Route]. + * + * **Precondition:** the [SSE] plugin must be installed on the application before calling this function. + * Use [Application.mcp] if you want SSE to be installed automatically. * - * @param path the URL path to register the route for SSE. - * @param block the block of code that defines the server's behavior for the SSE session. + * @param path the URL path to register the SSE endpoint. + * @param block factory block with access to the [ServerSSESession] + * that creates and returns the [Server] to handle the connection. + * @throws IllegalStateException if the [SSE] plugin is not installed. */ @KtorDsl public fun Route.mcp(path: String, block: ServerSSESession.() -> Server) { @@ -61,11 +47,14 @@ public fun Route.mcp(path: String, block: ServerSSESession.() -> Server) { } /** - * Configures the Ktor Application to handle Model Context Protocol (MCP) over Server-Sent Events (SSE). + * Registers MCP over [Server-Sent Events (SSE) Transport](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) + * endpoints on this [Route]. * * **Precondition:** the [SSE] plugin must be installed on the application before calling this function. * Use [Application.mcp] if you want SSE to be installed automatically. * + * @param block factory block with access to the [ServerSSESession] + * that creates and returns the [Server] to handle the connection. * @throws IllegalStateException if the [SSE] plugin is not installed. */ @KtorDsl @@ -81,7 +70,7 @@ public fun Route.mcp(block: ServerSSESession.() -> Server) { ) } - val transportManager = TransportManager() + val transportManager = TransportManager() sse { mcpSseEndpoint("", transportManager, block) @@ -92,6 +81,14 @@ public fun Route.mcp(block: ServerSSESession.() -> Server) { } } +/** + * Configures the Ktor Application to handle Model Context Protocol (MCP) + * over [Server-Sent Events (SSE) Transport](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) + * and sets up routing with the provided configuration block. + * + * @param block factory block with access to the [ServerSSESession] + * that creates and returns the [Server] to handle the connection. + */ @KtorDsl public fun Application.mcp(block: ServerSSESession.() -> Server) { install(SSE) @@ -101,19 +98,14 @@ public fun Application.mcp(block: ServerSSESession.() -> Server) { } } -@KtorDsl -@Suppress("LongParameterList") -public fun Application.mcpStreamableHttp( +private fun Application.mcpStreamableHttp( path: String = "/mcp", - enableDnsRebindingProtection: Boolean = false, - allowedHosts: List? = null, - allowedOrigins: List? = null, - eventStore: EventStore? = null, + configuration: StreamableHttpServerTransport.Configuration, block: RoutingContext.() -> Server, ) { install(SSE) - val transportManager = TransportManager() + val transportManager = TransportManager() routing { route(path) { @@ -125,13 +117,9 @@ public fun Application.mcpStreamableHttp( post { val transport = streamableTransport( transportManager = transportManager, - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, + configuration = configuration, block = block, - ) - ?: return@post + ) ?: return@post transport.handleRequest(null, call) } @@ -144,15 +132,50 @@ public fun Application.mcpStreamableHttp( } } +/** + * Configures the Ktor Application to handle Model Context Protocol (MCP) + * over [Streamable HTTP Transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) + * + * Sets up SSE, HTTP POST, and DELETE endpoints at the specified [path]. + * Simple request/response pairs are returned as JSON (not SSE streams). + * + * @param path The base path for the MCP Streamable HTTP endpoint. Defaults to "/mcp". + * @param enableDnsRebindingProtection Enables DNS rebinding attack protection for the endpoint. Defaults to false. + * @param allowedHosts A list of hostnames allowed to access the endpoint. If `null`, no restrictions are applied. + * @param allowedOrigins A list of origins allowed to perform cross-origin requests (CORS). + * If `null`, no restrictions are applied. + * @param eventStore An optional [EventStore] instance to enable resumable event stream functionality. + * Allows storing and replaying events. + * @param block factory block with access to the [RoutingContext] (for reading request headers) + * that creates and returns the [Server] to handle the connection. + */ @KtorDsl @Suppress("LongParameterList") -public fun Application.mcpStatelessStreamableHttp( +public fun Application.mcpStreamableHttp( path: String = "/mcp", enableDnsRebindingProtection: Boolean = false, allowedHosts: List? = null, allowedOrigins: List? = null, eventStore: EventStore? = null, block: RoutingContext.() -> Server, +) { + mcpStreamableHttp( + path = path, + configuration = StreamableHttpServerTransport.Configuration( + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + eventStore = eventStore, + enableJsonResponse = true, + ), + block = block, + ) +} + +private fun Application.mcpStatelessStreamableHttp( + path: String = "/mcp", + configuration: StreamableHttpServerTransport.Configuration, + block: RoutingContext.() -> Server, ) { install(SSE) @@ -160,10 +183,7 @@ public fun Application.mcpStatelessStreamableHttp( route(path) { post { mcpStatelessStreamableHttpEndpoint( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, + configuration = configuration, block = block, ) } @@ -185,9 +205,47 @@ public fun Application.mcpStatelessStreamableHttp( } } +/** + * Configures the Ktor Application to handle Model Context Protocol (MCP) + * over _stateless_ [Streamable HTTP Transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) + * + * Sets up an HTTP POST endpoint at [path]. GET and DELETE requests return 405 Method Not Allowed. + * Simple request/response pairs are returned as JSON (not SSE streams). + * + * @param path The URL path where the server listens for incoming JSON-RPC requests. Defaults to "/mcp". + * @param enableDnsRebindingProtection Determines whether DNS rebinding protection is enabled. Defaults to `false`. + * @param allowedHosts A list of allowed hostnames. If null, host filtering is disabled. + * @param allowedOrigins A list of allowed origins for CORS. If null, origin filtering is disabled. + * @param eventStore An optional [EventStore] implementation to provide resumability and event replay support. + * @param block factory block with access to the [RoutingContext] (for reading request headers) + * that creates and returns the [Server] to handle the connection. + */ +@KtorDsl +@Suppress("LongParameterList") +public fun Application.mcpStatelessStreamableHttp( + path: String = "/mcp", + enableDnsRebindingProtection: Boolean = false, + allowedHosts: List? = null, + allowedOrigins: List? = null, + eventStore: EventStore? = null, + block: RoutingContext.() -> Server, +) { + mcpStatelessStreamableHttp( + path = path, + configuration = StreamableHttpServerTransport.Configuration( + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, + eventStore = eventStore, + enableJsonResponse = true, + ), + block = block, + ) +} + private suspend fun ServerSSESession.mcpSseEndpoint( postEndpoint: String, - transportManager: TransportManager, + transportManager: TransportManager, block: ServerSSESession.() -> Server, ) { val transport = mcpSseTransport(postEndpoint, transportManager) @@ -208,7 +266,7 @@ private suspend fun ServerSSESession.mcpSseEndpoint( private fun ServerSSESession.mcpSseTransport( postEndpoint: String, - transportManager: TransportManager, + transportManager: TransportManager, ): SseServerTransport { val transport = SseServerTransport(postEndpoint, this) transportManager.addTransport(transport.sessionId, transport) @@ -218,20 +276,11 @@ private fun ServerSSESession.mcpSseTransport( } private suspend fun RoutingContext.mcpStatelessStreamableHttpEndpoint( - enableDnsRebindingProtection: Boolean = false, - allowedHosts: List? = null, - allowedOrigins: List? = null, - eventStore: EventStore? = null, + configuration: StreamableHttpServerTransport.Configuration, block: RoutingContext.() -> Server, ) { val transport = StreamableHttpServerTransport( - StreamableHttpServerTransport.Configuration( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, - enableJsonResponse = true, - ), + configuration, ).also { it.setSessionIdGenerator(null) } logger.info { "New stateless StreamableHttp connection established without sessionId" } @@ -244,7 +293,7 @@ private suspend fun RoutingContext.mcpStatelessStreamableHttpEndpoint( logger.debug { "Server connected to transport without sessionId" } } -private suspend fun RoutingContext.mcpPostEndpoint(transportManager: TransportManager) { +private suspend fun RoutingContext.mcpPostEndpoint(transportManager: TransportManager) { val sessionId: String = call.request.queryParameters["sessionId"] ?: run { call.respond(HttpStatusCode.BadRequest, "sessionId query parameter is not provided") return @@ -252,7 +301,7 @@ private suspend fun RoutingContext.mcpPostEndpoint(transportManager: TransportMa logger.debug { "Received message for sessionId: $sessionId" } - val transport = transportManager.getTransport(sessionId) as SseServerTransport? + val transport = transportManager.getTransport(sessionId) if (transport == null) { logger.warn { "Session not found for sessionId: $sessionId" } call.respond(HttpStatusCode.NotFound, "Session not found") @@ -267,7 +316,7 @@ private fun ApplicationRequest.sessionId(): String? = header(MCP_SESSION_ID_HEAD private suspend fun existingStreamableTransport( call: ApplicationCall, - transportManager: TransportManager, + transportManager: TransportManager, ): StreamableHttpServerTransport? { val sessionId = call.request.sessionId() if (sessionId.isNullOrEmpty()) { @@ -279,42 +328,31 @@ private suspend fun existingStreamableTransport( return null } - val transport = transportManager.getTransport(sessionId) as? StreamableHttpServerTransport - if (transport == null) { + val transport = transportManager.getTransport(sessionId) + return if (transport == null) { call.reject( HttpStatusCode.NotFound, RPCError.ErrorCode.CONNECTION_CLOSED, "Session not found", ) - return null + null + } else { + transport } - - return transport } private suspend fun RoutingContext.streamableTransport( - transportManager: TransportManager, - enableDnsRebindingProtection: Boolean, - allowedHosts: List?, - allowedOrigins: List?, - eventStore: EventStore?, + transportManager: TransportManager, + configuration: StreamableHttpServerTransport.Configuration, block: RoutingContext.() -> Server, ): StreamableHttpServerTransport? { val sessionId = call.request.sessionId() if (sessionId != null) { - val transport = transportManager.getTransport(sessionId) as? StreamableHttpServerTransport + val transport = transportManager.getTransport(sessionId) return transport ?: existingStreamableTransport(call, transportManager) } - val transport = StreamableHttpServerTransport( - StreamableHttpServerTransport.Configuration( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, - eventStore = eventStore, - enableJsonResponse = true, - ), - ) + val transport = StreamableHttpServerTransport(configuration) transport.setOnSessionInitialized { initializedSessionId -> transportManager.addTransport(initializedSessionId, transport) diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/TransportManager.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/TransportManager.kt new file mode 100644 index 000000000..e02931f6a --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/TransportManager.kt @@ -0,0 +1,22 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import kotlinx.atomicfu.AtomicRef +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.update +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.toPersistentMap + +internal class TransportManager { + private val transports: AtomicRef> = atomic(emptyMap().toPersistentMap()) + + fun getTransport(sessionId: String): T? = transports.value[sessionId] + + fun addTransport(sessionId: String, transport: T) { + transports.update { it.put(sessionId, transport) } + } + + fun removeTransport(sessionId: String) { + transports.update { it.remove(sessionId) } + } +} From a38818018ac8570973b502600938544696fa6248 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:10:25 +0200 Subject: [PATCH 011/187] fix!(server): #575 Rethrow CancellationException in StdioServerTransport, fix ReadBuffer (#571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Rethrow CancellationException in StdioServerTransport, fix ReadBuffer ### Changes 1. StdioServerTransport - Replace generic `Throwable` catches with specific `CancellationException` and `Exception` for clarity and correctness. **breaking change** `Throwable` is not handled any more! - Refactor launching jobs to separate methods - Introduce `READ_BUFFER_SIZE` constant to replace inline buffer size literals. - Add suppress annotations for clearer intent and constructor documentation for better usability. - Extend StdioServerTransportTest 2. ReadBuffer Previously, readMessage() returned null after consuming an unparseable line even when more complete lines existed in the buffer. This caused valid messages following a bad line in the same chunk to be silently dropped until the next chunk arrived (or forever, in tests). Fix the loop so null means only "no complete line available", not "encountered a parse failure". Blank/whitespace-only lines are now silently skipped via isBlank() rather than forwarded to the deserializer and logged as errors. Refactor the method into three focused helpers: - readMessage() — outer loop over lines - readLine() — consume the next newline-delimited line from the buffer - tryRecover() — attempt deserialization from the first '{' onward 3. Add utility method `runIntegrationTest` for integration testing (`runBlocking` + `withTimeout`). Use it in StdioServerTransportTest 4. Store processing Job reference and `calcelAndJoin()` it on close. Partially addressing #574 ## Motivation and Context See #575, #564, #242 ## How Has This Been Tested? Unit test added ## Breaking Changes Semantic change: `Throwable` and CancellationException are not handled any more! ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context --- .../kotlin/sdk/shared/ReadBuffer.kt | 87 +++--- .../kotlin/sdk/shared/ReadBufferTest.kt | 16 ++ kotlin-sdk-server/build.gradle.kts | 2 + .../kotlin/sdk/server/StdioServerTransport.kt | 93 ++++++- .../sdk/server/StdioServerTransportTest.kt | 256 ++++++++++++++++-- .../kotlin/test/utils/testing.kt | 29 ++ 6 files changed, 411 insertions(+), 72 deletions(-) create mode 100644 test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt index d991e7fe0..d3fc61fd4 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt @@ -20,45 +20,70 @@ public class ReadBuffer { buffer.write(chunk) } + /** + * Reads and deserializes a JSON-RPC message from the input buffer. + * + * The method attempts to read lines from the buffer until a valid `JSONRPCMessage` is successfully + * deserialized. Blank lines are ignored, and if a deserialization error occurs, the method attempts + * to recover and process the message. + * + * Recovery involves attempting to parse from the first detected JSON object in the line when an + * error is encountered during deserialization. + * + * @return A deserialized `JSONRPCMessage` if successfully processed; otherwise, `null` if the + * input buffer is exhausted or no valid message is found. + */ public fun readMessage(): JSONRPCMessage? { - if (buffer.exhausted()) return null - var lfIndex = buffer.indexOf('\n'.code.toByte()) - val line = when (lfIndex) { - -1L -> return null + while (true) { + val line = readNextLine() ?: return null + if (line.isBlank()) continue - 0L -> { - buffer.skip(1) - return null + @Suppress("TooGenericExceptionCaught") + val message = try { + deserializeMessage(line) + } catch (e: Exception) { + logger.error(e) { "Failed to deserialize message from line: $line\nAttempting to recover..." } + tryRecover(line) } - - else -> { - var skipBytes = 1 - if (buffer[lfIndex - 1] == '\r'.code.toByte()) { - lfIndex -= 1 - skipBytes += 1 - } - val string = buffer.readString(lfIndex) - buffer.skip(skipBytes.toLong()) - string + if (message != null) { + return message } } - try { - return deserializeMessage(line) - } catch (e: Exception) { - logger.error(e) { "Failed to deserialize message from line: $line\nAttempting to recover..." } - // if there is a non-JSON object prefix, try to parse from the first '{' onward. - val braceIndex = line.indexOf('{') - if (braceIndex != -1) { - val trimmed = line.substring(braceIndex) - try { - return deserializeMessage(trimmed) - } catch (ignored: Exception) { - logger.error(ignored) { "Deserialization failed for line: $line\nSkipping..." } - } + } + + private fun readNextLine(): String? { + val lfIndex = if (buffer.exhausted()) -1L else buffer.indexOf('\n'.code.toByte()) + if (lfIndex == -1L) return null + + return if (lfIndex == 0L) { + buffer.skip(1) + "" + } else { + var skipBytes = 1 + var messageLength = lfIndex + if (buffer[lfIndex - 1] == '\r'.code.toByte()) { + messageLength -= 1 + skipBytes += 1 } + val string = buffer.readString(messageLength) + buffer.skip(skipBytes.toLong()) + string } + } + + private fun tryRecover(line: String): JSONRPCMessage? { + // if there is a non-JSON object prefix, try to parse from the first '{' onward. + val braceIndex = line.indexOf('{') + if (braceIndex == -1) return null - return null + val trimmed = line.substring(braceIndex) + @Suppress("TooGenericExceptionCaught") + return try { + deserializeMessage(trimmed) + } catch (ignored: Exception) { + logger.error(ignored) { "Deserialization failed for line: $line\nSkipping..." } + null + } } public fun clear() { diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt index a49ff3df6..7b4f05308 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt @@ -31,6 +31,8 @@ class ReadBufferTest { val messageBytes = json.encodeToString(testMessage).encodeToByteArray() readBuffer.append(messageBytes) assertNull(readBuffer.readMessage()) + readBuffer.append("\r".encodeToByteArray()) + assertNull(readBuffer.readMessage()) // Append a newline and verify message is now available readBuffer.append("\n".encodeToByteArray()) @@ -45,6 +47,20 @@ class ReadBufferTest { assertNull(readBuffer.readMessage()) } + @Test + fun `skip blank line`() { + val readBuffer = ReadBuffer() + readBuffer.append(" \n".toByteArray()) + assertNull(readBuffer.readMessage()) + } + + @Test + fun `skip invalid json line`() { + val readBuffer = ReadBuffer() + readBuffer.append(" {ah=oh\n".toByteArray()) + assertNull(readBuffer.readMessage()) + } + @Test fun `should be reusable after clearing`() { val readBuffer = ReadBuffer() diff --git a/kotlin-sdk-server/build.gradle.kts b/kotlin-sdk-server/build.gradle.kts index 9d0bddff8..3a0a3985a 100644 --- a/kotlin-sdk-server/build.gradle.kts +++ b/kotlin-sdk-server/build.gradle.kts @@ -23,11 +23,13 @@ kotlin { implementation(libs.kotest.assertions.core) implementation(libs.kotest.assertions.json) implementation(libs.kotlinx.coroutines.test) + implementation(project(":test-utils")) } } jvmTest { dependencies { + implementation(libs.junit.jupiter.params) implementation(libs.kotest.assertions.ktor) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.client.logging) diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt index b3ec4d650..8fd46dd21 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt @@ -7,6 +7,7 @@ import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable @@ -26,25 +27,32 @@ import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.coroutines.CoroutineContext +private const val READ_BUFFER_SIZE = 8192L + /** * A server transport that communicates with a client via standard I/O. * - * Reads from System.in and writes to System.out. + * Reads from input [Source] and writes to output [Sink]. + * + * @constructor Creates a new instance of [StdioServerTransport]. + * @param inputStream The input [Source] used to receive data. + * @param outputStream The output [Sink] used to send data. */ @OptIn(ExperimentalAtomicApi::class) public class StdioServerTransport(private val inputStream: Source, outputStream: Sink) : AbstractTransport() { - private val logger = KotlinLogging.logger {} + private val logger = KotlinLogging.logger {} private val readBuffer = ReadBuffer() private val initialized: AtomicBoolean = AtomicBoolean(false) private var readingJob: Job? = null private var sendingJob: Job? = null + private var processingJob: Job? = null private val coroutineContext: CoroutineContext = IODispatcher + SupervisorJob() private val scope = CoroutineScope(coroutineContext) private val readChannel = Channel(Channel.UNLIMITED) private val writeChannel = Channel(Channel.UNLIMITED) - private val outputWriter = outputStream.buffered() + private val outputSink = outputStream.buffered() override suspend fun start() { if (!initialized.compareAndSet(expectedValue = false, newValue = true)) { @@ -52,11 +60,22 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: } // Launch a coroutine to read from stdin - readingJob = scope.launch { + readingJob = launchReadingJob() + + // Launch a coroutine to process messages from readChannel + processingJob = launchProcessingJob() + + // Launch a coroutine to handle message sending + sendingJob = launchSendingJob() + } + + private fun launchReadingJob(): Job { + val job = scope.launch { val buf = Buffer() + @Suppress("TooGenericExceptionCaught") try { while (isActive) { - val bytesRead = inputStream.readAtMostTo(buf, 8192) + val bytesRead = inputStream.readAtMostTo(buf, READ_BUFFER_SIZE) if (bytesRead == -1L) { // EOF reached break @@ -66,6 +85,8 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: readChannel.send(chunk) } } + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { logger.error(e) { "Error reading from stdin" } _onError.invoke(e) @@ -74,35 +95,59 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: close() } } + job.invokeOnCompletion { cause -> + logJobCompletion("Message reading", cause) + } + return job + } - // Launch a coroutine to process messages from readChannel - scope.launch { + private fun launchProcessingJob(): Job { + val job = scope.launch { + @Suppress("TooGenericExceptionCaught") try { for (chunk in readChannel) { readBuffer.append(chunk) processReadBuffer() } + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { _onError.invoke(e) } } + job.invokeOnCompletion { cause -> + logJobCompletion("Processing", cause) + } + return job + } - // Launch a coroutine to handle message sending - sendingJob = scope.launch { + private fun launchSendingJob(): Job { + val job = scope.launch { + @Suppress("TooGenericExceptionCaught") try { for (message in writeChannel) { val json = serializeMessage(message) - outputWriter.writeString(json) - outputWriter.flush() + outputSink.writeString(json) + outputSink.flush() } + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { logger.error(e) { "Error writing to stdout" } _onError.invoke(e) } } + job.invokeOnCompletion { cause -> + logJobCompletion("Message sending", cause) + if (cause is CancellationException) { + readingJob?.cancel(cause) + } + } + return job } private suspend fun processReadBuffer() { + @Suppress("TooGenericExceptionCaught") while (true) { val message = try { readBuffer.readMessage() @@ -115,12 +160,30 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: // Async invocation broke delivery order try { _onMessage.invoke(message) + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { + logger.error(e) { "Error processing message" } _onError.invoke(e) } } } + private fun logJobCompletion(jobName: String, cause: Throwable?) { + when (cause) { + is CancellationException -> { + } + + null -> { + logger.debug { "$jobName job completed" } + } + + else -> { + logger.debug(cause) { "$jobName job completed exceptionally" } + } + } + } + override suspend fun close() { if (!initialized.compareAndSet(expectedValue = true, newValue = false)) return @@ -133,13 +196,15 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: }.onFailure { logger.warn(it) { "Failed to close stdin" } } readingJob?.cancel() - readChannel.close() + + processingJob?.cancelAndJoin() + readBuffer.clear() runCatching { - outputWriter.flush() - outputWriter.close() + outputSink.flush() + outputSink.close() }.onFailure { logger.warn(it) { "Failed to close stdout" } } invokeOnCloseCallback() diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt index ff46263d2..dcbb7a4b8 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt @@ -1,28 +1,44 @@ package io.modelcontextprotocol.kotlin.sdk.server +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.PingRequest import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import io.modelcontextprotocol.kotlin.test.utils.runIntegrationTest +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.withTimeout +import kotlinx.io.Buffer +import kotlinx.io.RawSink +import kotlinx.io.RawSource 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.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource import java.io.ByteArrayOutputStream +import java.io.IOException 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 +import kotlin.test.fail +import kotlin.time.Duration.Companion.seconds +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class StdioServerTransportTest { private lateinit var input: PipedInputStream private lateinit var inputWriter: PipedOutputStream @@ -33,7 +49,7 @@ class StdioServerTransportTest { private lateinit var bufferedInput: Source private lateinit var printOutput: Sink - @BeforeTest + @BeforeEach fun setUp() { // Simulate an input stream that we can push data into using inputWriter. input = PipedInputStream() @@ -50,33 +66,36 @@ class StdioServerTransportTest { } bufferedInput = input.asSource().buffered() - printOutput = output.asSink().buffered() } @Test - fun `should start then close cleanly`() { - runBlocking { - val server = StdioServerTransport(bufferedInput, printOutput) - server.onError { error -> - throw error - } - - var didClose = false - server.onClose { - didClose = true - } + fun `should be safe to close before start`() = runIntegrationTest { + val server = StdioServerTransport(bufferedInput, printOutput) + server.close() // initialized guard makes this a no-op; must not throw + } - server.start() - assertFalse(didClose, "Should not have closed yet") + @Test + fun `should start then close cleanly`() = runIntegrationTest { + val server = StdioServerTransport(bufferedInput, printOutput) + server.onError { error -> + throw error + } - server.close() - assertTrue(didClose, "Should have closed after calling close()") + var didClose = false + server.onClose { + didClose = true } + + server.start() + assertFalse(didClose, "Should not have closed yet") + + server.close() + assertTrue(didClose, "Should have closed after calling close()") } @Test - fun `should not read until started`() = runTest { + fun `should not read until started`() = runIntegrationTest { val server = StdioServerTransport(bufferedInput, printOutput) server.onError { error -> throw error @@ -101,11 +120,11 @@ class StdioServerTransportTest { server.start() val received = readMessage.await() - assertEquals(message, received) + received shouldBe message } @Test - fun `should read multiple messages`() = runTest { + fun `should read multiple messages`() = runIntegrationTest { val server = StdioServerTransport(bufferedInput, printOutput) server.onError { error -> throw error @@ -135,10 +154,193 @@ class StdioServerTransportTest { server.start() finished.await() - assertEquals(messages, readMessages) + readMessages shouldBe messages + } + + // region: Exception handling + + @ParameterizedTest(name = "[{index}] input throws {0}") + @MethodSource("inputErrors") + fun `should invoke onError when input stream throws`(throwable: Throwable): Unit = runIntegrationTest { + val server = StdioServerTransport(FaultyRawSource(throwable).buffered(), printOutput) + val capturedError = CompletableDeferred() + server.onError { capturedError.complete(it) } + server.onMessage {} + + server.start() + + capturedError.await() shouldBe throwable + server.close() + } + + @ParameterizedTest(name = "[{index}] output throws {0}") + @MethodSource("outputErrors") + fun `should invoke onError when output sink throws`(throwable: Throwable): Unit = runIntegrationTest { + val server = StdioServerTransport(bufferedInput, FaultyRawSink(throwable).buffered()) + val capturedError = CompletableDeferred() + server.onError { capturedError.complete(it) } + server.onMessage {} + + server.start() + server.send(PingRequest().toJSON()) + + capturedError.await() shouldBe throwable + server.close() + } + + @Test + fun `should call onClose when input EOF is reached`(): Unit = runIntegrationTest { + val server = StdioServerTransport(bufferedInput, printOutput) + val didClose = CompletableDeferred() + server.onError { throw it } + server.onClose { didClose.complete(Unit) } + server.onMessage {} + + server.start() + inputWriter.close() // signal EOF to the reading loop + + eventually(2.seconds) { + didClose.isCompleted shouldBe true + } + } + + @Test + fun `should throw when starting twice`(): Unit = runIntegrationTest { + val server = StdioServerTransport(bufferedInput, printOutput) + server.onMessage {} + server.start() + withClue("Server should not start twice") { + shouldThrow { + server.start() + } + } + server.close() + } + + @ParameterizedTest(name = "[{index}] handler throws {0}") + @MethodSource("handlerErrors") + fun `should continue processing messages after handler throws`(throwable: Throwable) = runIntegrationTest { + val server = StdioServerTransport(bufferedInput, printOutput) + val capturedErrors = mutableListOf() + val receivedMessages = mutableListOf() + val secondMessageProcessed = CompletableDeferred() + + val message1 = PingRequest().toJSON() + val message2 = InitializedNotification().toJSON() + + server.onError { capturedErrors.add(it) } + server.onMessage { message -> + if (message == message1) { + throw throwable + } else { + receivedMessages.add(message) + secondMessageProcessed.complete(Unit) + } + } + + server.start() + + inputWriter.write(serializeMessage(message1)) + inputWriter.write(serializeMessage(message2)) + inputWriter.flush() + + secondMessageProcessed.await() + + capturedErrors shouldContain throwable + receivedMessages shouldBe listOf(message2) + server.close() + } + + @Test + fun `should not invoke onError for CancellationException in handler`() = runIntegrationTest { + val server = StdioServerTransport(bufferedInput, printOutput) + val capturedError = CompletableDeferred() + server.onError { capturedError.complete(it) } + + server.onMessage { throw CancellationException("cancelled") } + server.start() + + inputWriter.write(serializeMessage(PingRequest().toJSON())) + inputWriter.flush() + + // We expect onError NOT to be called. + // We wait a bit to make sure it's not called, then close. + try { + withTimeout(1.seconds) { + capturedError.await() + } + fail("Should not have captured an error for CancellationException") + } catch (_: TimeoutCancellationException) { + // Success - timeout reached without error captured + } finally { + server.close() + } + } + + // endregion + + @Test + fun `should continue receiving valid messages after malformed JSON is skipped`() = runIntegrationTest { + val server = StdioServerTransport(bufferedInput, printOutput) + val received = CompletableDeferred() + // ReadBuffer silently skips unparseable lines — no onError callback expected + server.onError {} + server.onMessage { received.complete(it) } + + val validMessage = PingRequest().toJSON() + inputWriter.write("not-valid-json\n".toByteArray()) + inputWriter.write(" \t \r \n".toByteArray()) // blank line + inputWriter.write(serializeMessage(validMessage).toByteArray()) + inputWriter.flush() + + server.start() + + received.await() shouldBe validMessage + server.close() + } + + @Suppress("unused") + private fun inputErrors() = listOf( + IOException("simulated read failure"), + RuntimeException("unexpected read exception"), + OutOfMemoryError("unexpected read error"), + ) + + @Suppress("unused") + private fun outputErrors() = listOf( + IOException("simulated write failure"), + RuntimeException("unexpected write exception"), + OutOfMemoryError("unexpected write error"), + ) + + @Suppress("unused") + private fun handlerErrors() = listOf( + RuntimeException("handler failure"), + IOException("handler IO failure"), + OutOfMemoryError("handler error"), + ) + + /** A [RawSource] that immediately throws [throwable] on every read attempt. */ + private class FaultyRawSource(private val throwable: Throwable) : RawSource { + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long = throw throwable + override fun close() { + // noop + } + } + + /** A [RawSink] that throws [throwable] on every [write] call. */ + private class FaultyRawSink(private val throwable: Throwable) : RawSink { + override fun write(source: Buffer, byteCount: Long): Unit = throw throwable + override fun flush() { + // noop + } + + override fun close() { + // noop + } } } -fun PipedOutputStream.write(s: String) { +private fun PipedOutputStream.write(s: String) { write(s.toByteArray()) } diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt new file mode 100644 index 000000000..ee7316254 --- /dev/null +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt @@ -0,0 +1,29 @@ +package io.modelcontextprotocol.kotlin.test.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import kotlin.coroutines.CoroutineContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +@Suppress("InjectDispatcher") // this is constant +private val defaultTestDispatcher: CoroutineContext = Dispatchers.IO.limitedParallelism(100) +private val defaultTestTimeout: Duration = 5.seconds + +/** + * Executes a suspendable integration test block within a coroutine scope, applying a timeout constraint. + * + * @param timeout The maximum duration allowed for the test execution. Defaults to 5 seconds. + * @param context The coroutine context within which the test block is executed. + * Defaults to [Dispatchers.IO] with 100 parallel tasks. + * @param block The suspendable test logic to be executed. + */ +public fun runIntegrationTest( + context: CoroutineContext = defaultTestDispatcher, + timeout: Duration = defaultTestTimeout, + block: suspend CoroutineScope.() -> Unit, +): Unit = runBlocking(context) { + withTimeout(timeout, block) +} From 3e7ea39bf70556154699bdfe483ca049a16b99aa Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:46:32 +0200 Subject: [PATCH 012/187] chore: relax Detekt rules for tests (#582) - Updated `detekt.yml` to exclude test folders from `EmptyFunctionBlock`, `LargeClass`, and `LongMethod` checks. - Cleanup detekt-baseline files. - Simplified `.idea/detekt.xml`. ## Motivation and Context To reduce linter noise and reduce tension ## How Has This Been Tested? `./gradlew detekt` ## Breaking Changes No ## Types of changes - [x] Build tool/ci update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context #567 --- .idea/detekt.xml | 7 +------ config/detekt/detekt.yml | 15 +++++++++++++++ .../detekt-baseline-commonTestSourceSet.xml | 11 ----------- integration-test/detekt-baseline-test.xml | 15 --------------- .../detekt-baseline-commonTestSourceSet.xml | 1 - kotlin-sdk-client/detekt-baseline-test.xml | 2 -- 6 files changed, 16 insertions(+), 35 deletions(-) delete mode 100644 integration-test/detekt-baseline-commonTestSourceSet.xml diff --git a/.idea/detekt.xml b/.idea/detekt.xml index a408ca926..d89f9080f 100644 --- a/.idea/detekt.xml +++ b/.idea/detekt.xml @@ -1,14 +1,9 @@ - - \ No newline at end of file + diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index a7504c1db..8c19749c2 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -3,3 +3,18 @@ config: warningsAsErrors: false checkExhaustiveness: false +complexity: + LargeClass: + excludes: &testFolders + - '**/test/**' + - '**/commonTest/**' + - '**/jvmTest/**' + - '**/jsTest/**' + - '**/iosTest/**' + LongMethod: + excludes: *testFolders + +empty-blocks: + EmptyFunctionBlock: + excludes: *testFolders + diff --git a/integration-test/detekt-baseline-commonTestSourceSet.xml b/integration-test/detekt-baseline-commonTestSourceSet.xml deleted file mode 100644 index 660fa6641..000000000 --- a/integration-test/detekt-baseline-commonTestSourceSet.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - EmptyFunctionBlock:ClientTest.kt:ClientTest.<no name provided>${ } - EmptyFunctionBlock:ClientTest.kt:ClientTest.<no name provided>${} - LargeClass:ClientTest.kt:ClientTest - LongMethod:ClientTest.kt:ClientTest$@Test fun `JSONRPCRequest with ToolsList method and default params returns list of tools` - LongMethod:ClientTest.kt:ClientTest$@Test fun `should handle logging setLevel request` - - diff --git a/integration-test/detekt-baseline-test.xml b/integration-test/detekt-baseline-test.xml index 1e93fe085..710b72936 100644 --- a/integration-test/detekt-baseline-test.xml +++ b/integration-test/detekt-baseline-test.xml @@ -7,8 +7,6 @@ AbstractClassCanBeConcreteClass:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$AbstractToolIntegrationTest AbstractClassCanBeConcreteClass:BaseTransportTest.kt:BaseTransportTest$BaseTransportTest CyclomaticComplexMethod:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$private fun setupCalculatorTool - EmptyFunctionBlock:ClientTest.kt:ClientTest.<no name provided>${ } - EmptyFunctionBlock:ClientTest.kt:ClientTest.<no name provided>${} ForbiddenComment:StdioClientTransportTest.kt:StdioClientTransportTest$// TODO: fix running on windows InjectDispatcher:AbstractKotlinClientTsServerTest.kt:AbstractKotlinClientTsServerTest$IO InjectDispatcher:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$IO @@ -21,19 +19,6 @@ InjectDispatcher:StreamableHttpIntegrationTest.kt:StreamableHttpIntegrationTest$IO InjectDispatcher:TsEdgeCasesTestSse.kt:TsEdgeCasesTestSse$IO InjectDispatcher:WebSocketIntegrationTest.kt:WebSocketIntegrationTest$IO - LargeClass:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest : KotlinTestBase - LargeClass:ClientTest.kt:ClientTest - LongMethod:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$@Test fun testMissingRequiredArguments - LongMethod:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$override fun configureServer - LongMethod:AbstractResourceIntegrationTest.kt:AbstractResourceIntegrationTest$override fun configureServer - LongMethod:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$private fun setupCalculatorTool - LongMethod:ClientTest.kt:ClientTest$@Test fun `JSONRPCRequest with ToolsList method and default params returns list of tools` - LongMethod:ClientTest.kt:ClientTest$@Test fun `should handle logging setLevel request` - LongMethod:KotlinServerForTsClientSse.kt:HttpServerTransport$suspend fun handleRequest - LongMethod:KotlinServerForTsClientSse.kt:KotlinServerForTsClient$fun createMcpServer: Server - LongMethod:KotlinServerForTsClientSse.kt:KotlinServerForTsClient$fun start - LongMethod:ServerResourcesNotificationSubscribeTest.kt:ServerResourcesNotificationSubscribeTest$@Test fun `should send resource notifications` - LongMethod:TsEdgeCasesTestSse.kt:TsEdgeCasesTestSse$@Test @Timeout(30, unit = TimeUnit.SECONDS) fun testComplexConcurrentRequests: Unit MatchingDeclarationName:PromptIntegrationTestSse.kt:SchemaPromptIntegrationTestSse : AbstractPromptIntegrationTest SleepInsteadOfDelay:KotlinServerForTsClientSse.kt:KotlinServerForTsClient$sleep(500) ThrowsCount:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$override fun configureServer diff --git a/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml b/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml index b73eb9fba..cab432296 100644 --- a/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml +++ b/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml @@ -2,7 +2,6 @@ - LongMethod:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$@Ignore @Test fun testNotificationSchemaE2E MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""" MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}""" diff --git a/kotlin-sdk-client/detekt-baseline-test.xml b/kotlin-sdk-client/detekt-baseline-test.xml index 30a384a0d..b16e8eb1c 100644 --- a/kotlin-sdk-client/detekt-baseline-test.xml +++ b/kotlin-sdk-client/detekt-baseline-test.xml @@ -6,8 +6,6 @@ ForbiddenComment:StreamableHttpClientTest.kt:StreamableHttpClientTest$// TODO: how to get notifications via Client API? InjectDispatcher:StdioClientTransportErrorHandlingTest.kt:StdioClientTransportErrorHandlingTest$IO InjectDispatcher:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$Default - LongMethod:StreamableHttpClientTest.kt:StreamableHttpClientTest$@Test fun `test streamableHttpClient` - LongMethod:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$@Ignore @Test fun testNotificationSchemaE2E LongParameterList:MockMcp.kt:MockMcp$fun handleJSONRPCRequest LongParameterList:MockMcp.kt:MockMcp$fun handleWithResult LongParameterList:MockMcp.kt:MockMcp$fun onInitialize From 1454229ad92293c8533fb0f3fcefea08ade3bbb2 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:59:07 +0200 Subject: [PATCH 013/187] test: add serialization tests and guidelines for handling `null` and omitted fields (#583) --- AGENTS.md | 4 +++ .../kotlin/sdk/types/InitializeTest.kt | 28 +++++++++++++++++++ .../kotlin/sdk/types/JsonRpcTest.kt | 20 +++++++++++++ 3 files changed, 52 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 7b723e876..1646c55a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,6 +92,10 @@ Follow these rules to keep changes safe, comprehensible, and easy to maintain. - **mockk**: Mocking library - **Ktor MockEngine**: For HTTP client mocking (`io.ktor:ktor-client-mock`) - **Java tests**: Use JUnit5, Mockito, AssertJ core +- **Serialization test utilities** (`io.modelcontextprotocol.kotlin.test.utils`): + - `verifySerialization(value, json, expectedJson)` — serializes, asserts match, round-trips back; use for most serialization tests + - `verifyDeserialization(json, payload)` — deserializes from JSON, re-serializes, asserts match; returns the object for further assertions + - Always test both empty/null/omitted and non-null cases for nullable fields; `McpJson` has `explicitNulls = false` so null properties must be absent from JSON, not `null` ### Kotest Patterns diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt index 69c08789a..3da2a8bd9 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt @@ -189,6 +189,34 @@ class InitializeTest { ) } + @Test + fun `should serialize InitializeResult without meta - meta must be omitted`() { + val result = InitializeResult( + protocolVersion = "2025-03-26", + capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = false)), + serverInfo = Implementation(name = "test-server", version = "1.0.0"), + ) + + verifySerialization( + result, + McpJson, + """ + { + "protocolVersion": "2025-03-26", + "capabilities": { + "tools": { + "listChanged": false + } + }, + "serverInfo": { + "name": "test-server", + "version": "1.0.0" + } + } + """.trimIndent(), + ) + } + @Test fun `should deserialize InitializeResult from JSON`() { val json = """ diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt index db111fef7..7de360d4b 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt @@ -265,6 +265,26 @@ class JsonRpcTest { ) } + @Test + fun `should serialize JSONRPCResponse with EmptyResult - null meta must be omitted`() { + val response = JSONRPCResponse( + id = RequestId("call-2"), + result = EmptyResult(), // meta = null by default + ) + + verifySerialization( + response, + McpJson, + """ + { + "id": "call-2", + "result": {}, + "jsonrpc": "2.0" + } + """.trimIndent(), + ) + } + @Test fun `should deserialize JSONRPCResponse with EmptyResult`() { val json = """ From 5b58814613069babb1bbd5cab4572331eaed4b45 Mon Sep 17 00:00:00 2001 From: Jakub Amanowicz Date: Thu, 5 Mar 2026 13:24:07 +0100 Subject: [PATCH 014/187] feat: bump version to 0.9.0 (#576) bumps version to 0.9.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c4128f537..45b067c88 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,4 +14,4 @@ kotlin.mpp.enableCInteropCommonization=true kotlin.native.ignoreDisabledTargets=true group=io.modelcontextprotocol -version=0.8.3 +version=0.9.0 From 0b39f57cffe3affc55ee6d591910b8d356468cd1 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 10 Mar 2026 12:45:21 +0100 Subject: [PATCH 015/187] docs: streamable server sample (#595) - add a simple streamable http server example with auth - update readme: replace quickstart example from sse server to streamable server - add readme files for samples, simple-streamable-server, notebooks closes #170 ## How Has This Been Tested? knit and inspector ## Breaking Changes NaN ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .github/workflows/build.yml | 1 + .github/workflows/samples.yml | 1 + README.md | 84 ++--- docs/build.gradle.kts | 2 + samples/README.md | 55 ++++ samples/kotlin-mcp-client/README.md | 73 ++--- samples/kotlin-mcp-server/README.md | 110 +++---- samples/notebooks/README.md | 31 ++ samples/simple-streamable-server/README.md | 75 +++++ .../simple-streamable-server/build.gradle.kts | 29 ++ .../gradle.properties | 5 + .../gradle/libs.versions.toml | 22 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 46175 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + samples/simple-streamable-server/gradlew | 248 +++++++++++++++ samples/simple-streamable-server/gradlew.bat | 93 ++++++ .../settings.gradle.kts | 25 ++ .../sample/server/main.kt | 28 ++ .../sample/server/server.kt | 297 ++++++++++++++++++ samples/weather-stdio-server/README.md | 153 +++------ 20 files changed, 1085 insertions(+), 254 deletions(-) create mode 100644 samples/README.md create mode 100644 samples/notebooks/README.md create mode 100644 samples/simple-streamable-server/README.md create mode 100644 samples/simple-streamable-server/build.gradle.kts create mode 100644 samples/simple-streamable-server/gradle.properties create mode 100644 samples/simple-streamable-server/gradle/libs.versions.toml create mode 100644 samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/simple-streamable-server/gradlew create mode 100644 samples/simple-streamable-server/gradlew.bat create mode 100644 samples/simple-streamable-server/settings.gradle.kts create mode 100644 samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt create mode 100644 samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b769d109..f97721bc6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -180,6 +180,7 @@ jobs: sample: - kotlin-mcp-client - kotlin-mcp-server + - simple-streamable-server - weather-stdio-server name: "Build Sample: ${{ matrix.sample }}" diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index cdd98bb08..373f00a56 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -29,6 +29,7 @@ jobs: sample: - kotlin-mcp-client - kotlin-mcp-server + - simple-streamable-server - weather-stdio-server name: Build Sample (${{ matrix.sample }}) diff --git a/README.md b/README.md index 30d1b40d3..acb5c054c 100644 --- a/README.md +++ b/README.md @@ -21,34 +21,34 @@ standardized protocol interface. * [Overview](#overview) * [Installation](#installation) - * [Artifacts](#artifacts) - * [Gradle setup (JVM)](#gradle-setup-jvm) - * [Multiplatform](#multiplatform) - * [Ktor dependencies](#ktor-dependencies) + * [Artifacts](#artifacts) + * [Gradle setup (JVM)](#gradle-setup-jvm) + * [Multiplatform](#multiplatform) + * [Ktor dependencies](#ktor-dependencies) * [Quickstart](#quickstart) - * [Creating a Client](#creating-a-client) - * [Creating a Server](#creating-a-server) + * [Creating a Client](#creating-a-client) + * [Creating a Server](#creating-a-server) * [Core Concepts](#core-concepts) - * [MCP Primitives](#mcp-primitives) - * [Capabilities](#capabilities) - * [Server Capabilities](#server-capabilities) - * [Client Capabilities](#client-capabilities) - * [Server Features](#server-features) - * [Prompts](#prompts) - * [Resources](#resources) - * [Tools](#tools) - * [Completion](#completion) - * [Logging](#logging) - * [Pagination](#pagination) - * [Client Features](#client-features) - * [Roots](#roots) - * [Sampling](#sampling) + * [MCP Primitives](#mcp-primitives) + * [Capabilities](#capabilities) + * [Server Capabilities](#server-capabilities) + * [Client Capabilities](#client-capabilities) + * [Server Features](#server-features) + * [Prompts](#prompts) + * [Resources](#resources) + * [Tools](#tools) + * [Completion](#completion) + * [Logging](#logging) + * [Pagination](#pagination) + * [Client Features](#client-features) + * [Roots](#roots) + * [Sampling](#sampling) * [Transports](#transports) - * [STDIO Transport](#stdio-transport) - * [Streamable HTTP Transport](#streamable-http-transport) - * [SSE Transport](#sse-transport) - * [WebSocket Transport](#websocket-transport) - * [ChannelTransport (testing)](#channeltransport-testing) + * [STDIO Transport](#stdio-transport) + * [Streamable HTTP Transport](#streamable-http-transport) + * [SSE Transport](#sse-transport) + * [WebSocket Transport](#websocket-transport) + * [ChannelTransport (testing)](#channeltransport-testing) * [Connecting your server](#connecting-your-server) * [Examples](#examples) * [Documentation](#documentation) @@ -183,17 +183,24 @@ fun main(args: Array) = runBlocking { ### Creating a Server -Create an MCP server that exposes a simple tool and runs on an embedded Ktor server with SSE transport: +Create an MCP server that exposes a simple tool and runs on an embedded Ktor server with Streamable HTTP transport. +For a full working project with all required dependencies, see +the [simple-streamable-server](samples/simple-streamable-server) sample. + ```kotlin +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.install import io.ktor.server.cio.CIO import io.ktor.server.engine.embeddedServer +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation 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.server.mcpStreamableHttp import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.McpJson import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema @@ -228,7 +235,10 @@ fun main(args: Array) { } embeddedServer(CIO, host = "127.0.0.1", port = port) { - mcp { + install(ContentNegotiation) { + json(McpJson) + } + mcpStreamableHttp { mcpServer } }.start(wait = true) @@ -243,7 +253,7 @@ You can run the server and then connect to it using the client or test with the npx -y @modelcontextprotocol/inspector ``` -In the inspector UI, connect to `http://localhost:3000`. +In the inspector UI, connect to `http://localhost:3000/mcp`. ## Core Concepts @@ -787,6 +797,7 @@ private class MyServer : fun main() { --> + ```kotlin embeddedServer(CIO, port = 3000) { mcpStreamableHttp(path = "/api/mcp") { @@ -794,6 +805,7 @@ embeddedServer(CIO, port = 3000) { } }.start(wait = true) ``` + @@ -831,6 +843,7 @@ private class MyServer : fun main() { --> + ```kotlin embeddedServer(CIO, port = 3000) { install(SSE) @@ -841,6 +854,7 @@ embeddedServer(CIO, port = 3000) { } }.start(wait = true) ``` + @@ -880,12 +894,9 @@ allowing for easy testing of MCP functionality without the need for network setu ## Examples -| Scenario | Description | Example | -|--------------------------|-----------------------------------------------------------------|--------------------------------------------------------------------------| -| Streamable HTTP server | Full MCP server with prompts, resources, tools, completions | [samples/kotlin-mcp-server](./samples/kotlin-mcp-server) | -| STDIO weather server | Minimal STDIO transport server exposing weather info and alerts | [samples/weather-stdio-server](./samples/weather-stdio-server) | -| Interactive STDIO client | MCP client that connects over STDIO and pipes requests to LLMs | [samples/kotlin-mcp-client](./samples/kotlin-mcp-client) | -| Streamable HTTP client | MCP client demo in a runnable notebook | [samples/notebooks/McpClient.ipynb](./samples/notebooks/McpClient.ipynb) | +The [samples](./samples) directory contains runnable projects demonstrating +MCP server and client implementations with various transports. +See the [samples overview](./samples/README.md) for a comparison table and detailed descriptions. ## Documentation @@ -899,4 +910,5 @@ Please see the [contribution guide](CONTRIBUTING.md) and the [Code of conduct](C ## License -This project is licensed under Apache 2.0 for new contributions, with existing code under MIT—see the [LICENSE](LICENSE) file for details. +This project is licensed under Apache 2.0 for new contributions, with existing code under MIT—see the [LICENSE](LICENSE) +file for details. diff --git a/docs/build.gradle.kts b/docs/build.gradle.kts index ac249791a..1857ef1a3 100644 --- a/docs/build.gradle.kts +++ b/docs/build.gradle.kts @@ -6,6 +6,8 @@ plugins { dependencies { implementation(project(":kotlin-sdk")) implementation(libs.ktor.server.cio) + implementation(libs.ktor.serialization) + implementation(libs.ktor.server.content.negotiation) } tasks.matching { diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 000000000..97893782d --- /dev/null +++ b/samples/README.md @@ -0,0 +1,55 @@ +# Kotlin MCP SDK Samples + +Runnable projects demonstrating MCP server and client implementations with the +[Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk). +For background on the protocol itself, see the [MCP documentation](https://modelcontextprotocol.io/introduction). + +## Overview + +| Sample | Type | Transport | MCP Features | +|--------------------------------------------------------|-------------------|-----------------|------------------------------------| +| [simple-streamable-server](./simple-streamable-server) | Server | Streamable HTTP | Tools, Resources, Prompts, Logging | +| [kotlin-mcp-server](./kotlin-mcp-server) | Server | STDIO, SSE | Tools, Resources, Prompts | +| [weather-stdio-server](./weather-stdio-server) | Server | STDIO | Tools | +| [kotlin-mcp-client](./kotlin-mcp-client) | Client | STDIO | Tool discovery & invocation | +| [notebooks](./notebooks) | Client (Notebook) | Streamable HTTP | Tool discovery & invocation | + +## Getting Started + +- **Building a server?** Start with [simple-streamable-server](./simple-streamable-server) — it + uses the recommended Streamable HTTP transport and covers tools, resources, prompts, and logging. +- **Building a client?** Open the [notebooks](./notebooks) sample for a step-by-step walkthrough, + or see [kotlin-mcp-client](./kotlin-mcp-client) for a full CLI client with Anthropic API + integration. + +## Samples + +### Simple Streamable HTTP Server + +A minimal Streamable HTTP server with optional Bearer token authentication. Demonstrates tools +(`greet`, `multi-greet`), a prompt template, a resource, and server-to-client logging notifications. +[Read more →](./simple-streamable-server) + +### Kotlin MCP Server + +A multi-transport server supporting STDIO, SSE (plain), and SSE (Ktor plugin). Useful for exploring +different transport modes side by side. +[Read more →](./kotlin-mcp-server) + +### Weather STDIO Server + +A focused STDIO server that exposes weather forecast and alert tools backed by the weather.gov API. +Includes Claude Desktop integration instructions. +[Read more →](./weather-stdio-server) + +### Kotlin MCP Client + +An interactive CLI client that connects to any MCP server over STDIO and routes queries through +Anthropic's Claude API, bridging MCP tools with LLM conversations. +[Read more →](./kotlin-mcp-client) + +### MCP Client Notebook + +A Kotlin notebook that connects to a remote MCP server via Streamable HTTP and demonstrates ping, +tool listing, and tool invocation — all in an interactive cell-by-cell format. +[Read more →](./notebooks) diff --git a/samples/kotlin-mcp-client/README.md b/samples/kotlin-mcp-client/README.md index fd9e14313..b10f9254a 100644 --- a/samples/kotlin-mcp-client/README.md +++ b/samples/kotlin-mcp-client/README.md @@ -1,69 +1,50 @@ # Kotlin MCP Client -This project demonstrates how to build a Model Context Protocol (MCP) client in Kotlin that interacts with an MCP server -via a STDIO transport layer while leveraging Anthropic's API for natural language processing. The client uses the MCP -Kotlin SDK to communicate with an MCP server that exposes various tools, and it uses Anthropic's API to process user -queries and integrate tool responses into the conversation. - -For more information about the MCP SDK and protocol, please refer to -the [MCP documentation](https://modelcontextprotocol.io/introduction). - -## Prerequisites - -- **Java 17 or later** -- **Gradle** (or the Gradle wrapper provided with the project) -- An Anthropic API key set in your environment variable `ANTHROPIC_API_KEY` -- Basic understanding of MCP concepts and Kotlin programming +An interactive CLI client that connects to any MCP server over STDIO and pipes queries through +Anthropic's Claude API. ## Overview -The client application performs the following tasks: +This sample demonstrates a complete MCP client workflow: launching an MCP server as a subprocess, +discovering its tools, converting them to Anthropic's tool format, and running an interactive chat +loop where Claude can call server tools on behalf of the user. -- **Connecting to an MCP server** — - launches an MCP server process (implemented in JavaScript, Python, or Java) using STDIO transport. - It connects to the server, retrieves available tools, and converts them to Anthropic’s tool format. -- **Processing queries** — - accepts user queries, sends them to Anthropic’s API along with the registered tools, and handles responses. - If the response indicates a tool should be called, it invokes the corresponding MCP tool and continues the - conversation based on the tool’s result. -- **Interactive chat loop** — - runs an interactive command-line loop, allowing users to continuously submit queries and receive responses. +## Prerequisites -## Building and Running +- JDK 17+ +- An `ANTHROPIC_API_KEY` environment variable set with a valid Anthropic API key +- An MCP server script to connect to (`.js`, `.py`, or `.jar`) -Use the Gradle wrapper to build the application. In a terminal, run: +## Build & Run -```shell -./gradlew clean build -``` +Run the client, passing the path to an MCP server: -To run the client, execute the jar file and provide the path to your MCP server script. +```shell +# Connect to a JVM server +./gradlew run --args="path/to/server.jar" -To run the client with any MCP server: +# Connect to a Python server +./gradlew run --args="path/to/server.py" -```shell -java -jar build/libs/.jar path/to/server.jar # jvm server -java -jar build/libs/.jar path/to/server.py # python server -java -jar build/libs/.jar path/to/build/index.js # node server +# Connect to a Node.js server +./gradlew run --args="path/to/build/index.js" ``` > [!NOTE] -> The client uses STDIO transport, so it launches the MCP server as a separate process. +> The client uses STDIO transport, so it launches the MCP server as a subprocess. > Ensure the server script is executable and is a valid `.js`, `.py`, or `.jar` file. -## Configuration for Anthropic +## MCP Capabilities -Ensure your Anthropic API key is available in your environment: - -```shell -export ANTHROPIC_API_KEY=your_anthropic_api_key_here -``` +From the **client** perspective, this sample demonstrates: -The client uses `AnthropicOkHttpClient.fromEnv()` to automatically load the API key from `ANTHROPIC_API_KEY` and -`ANTHROPIC_AUTH_TOKEN` environment variables. +- **Tool discovery** — lists tools from the connected server and converts them to Anthropic's tool + format. +- **Tool invocation** — when Claude's response requests a tool call, the client invokes the + corresponding MCP tool and feeds the result back into the conversation. ## Additional Resources -- [MCP Specification](https://spec.modelcontextprotocol.io/) +- [MCP Specification](https://modelcontextprotocol.io/specification/latest) - [Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk) -- [Anthropic Java SDK](https://github.com/anthropics/anthropic-sdk-java/tree/main) +- [Anthropic Java SDK](https://github.com/anthropics/anthropic-sdk-java) diff --git a/samples/kotlin-mcp-server/README.md b/samples/kotlin-mcp-server/README.md index fa56b66dc..82dc5e73e 100644 --- a/samples/kotlin-mcp-server/README.md +++ b/samples/kotlin-mcp-server/README.md @@ -1,93 +1,85 @@ -# MCP Kotlin Server Sample +# Kotlin MCP Server -A sample implementation of an MCP (Model Context Protocol) server in Kotlin that demonstrates different server -configurations and transport methods. +A sample MCP server demonstrating multiple transport modes: STDIO, SSE (plain), and SSE (Ktor +plugin). -## Features +> **Note:** The SSE transport modes are provided for backward compatibility. For new projects, +> consider using the [simple-streamable-server](../simple-streamable-server) sample, which uses the +> recommended Streamable HTTP transport. -- Multiple server operation modes: - - Standard I/O server - - SSE (Server-Sent Events) server with plain configuration - - SSE server using Ktor plugin -- Built-in capabilities for: - - Prompts management - - Resources handling - - Tools integration +## Overview -## Getting Started +This sample registers a prompt, a tool, and a resource, then lets you choose how to expose them. +STDIO mode is the default and is best for process-based clients. The two SSE modes show how to +serve MCP over HTTP using either a manual Ktor routing setup or the built-in `mcp { }` Ktor plugin. -### Running the Server +## Prerequisites -The server defaults [STDIO transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio). +- JDK 17+ +- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) (optional, for testing) -You can customize the behavior using command-line arguments. -Logs are printed to [./build/stdout.log](./build/stdout.log) +## Build & Run -#### Standard I/O mode (STDIO): +### STDIO mode (default) -```bash -./gradlew clean build +```shell +./gradlew run ``` -Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) -to connect to MCP via STDIO (Click the "▶️ Connect" button): + +Or explicitly: ```shell -npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server stdio-server +./gradlew run --args="--stdio" ``` -#### SSE with plain configuration: +Connect with the MCP Inspector: -**NB!: 🐞 This configuration may not work ATM** - -```bash -./gradlew run --args="--sse-server 3001" -``` -or ```shell -./gradlew clean build -java -jar ./build/libs/kotlin-mcp-server-0.1.0-all.jar --sse-server 3001 +npx @modelcontextprotocol/inspector --config samples/kotlin-mcp-server/mcp-inspector-config.json --server stdio-server ``` -Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) -to connect to `http://localhost:3002/` via SSE Transport (Click the "▶️ Connect" button): +### SSE with Ktor plugin + ```shell -npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server sse-server +./gradlew run --args="--sse-server-ktor 3002" ``` -#### SSE with Ktor plugin: +Connect with the MCP Inspector: -```bash -./gradlew run --args="--sse-server-ktor 3002" +```shell +npx @modelcontextprotocol/inspector --config samples/kotlin-mcp-server/mcp-inspector-config.json --server sse-ktor-server ``` -or + +### SSE with plain configuration + +> **Known issue:** This mode may not work correctly at this time. + ```shell -./gradlew clean build -java -jar ./build/libs/kotlin-mcp-server-0.1.0-all.jar --sse-server-ktor 3002 +./gradlew run --args="--sse-server 3001" ``` -Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) -to connect to `http://localhost:3002/` via SSE transport (Click the "▶️ Connect" button): +Connect with the MCP Inspector: + ```shell -npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server sse-ktor-server +npx @modelcontextprotocol/inspector --config samples/kotlin-mcp-server/mcp-inspector-config.json --server sse-server ``` -## Server Capabilities +## MCP Capabilities + +### Tools -- **Prompts**: Supports prompt management with list change notifications -- **Resources**: Includes subscription support and list change notifications -- **Tools**: Supports tool management with list change notifications +| Name | Description | +|-------------------|------------------------------------------------------| +| `kotlin-sdk-tool` | A test tool that returns a "Hello, world!" greeting. | -## Implementation Details +### Prompts -The server is implemented using: -- Ktor for HTTP server functionality (SSE modes) -- Kotlin coroutines for asynchronous operations -- SSE for real-time communication in web contexts -- Standard I/O for command-line interface and process-based communication +| Name | Description | +|--------------------|------------------------------------------------------------------------------------| +| `Kotlin Developer` | Generates a prompt to develop a small Kotlin application for a given project name. | -## Example Capabilities +### Resources -The sample server demonstrates: -- **Prompt**: "Kotlin Developer" - helps develop small Kotlin applications with a configurable project name -- **Tool**: "kotlin-sdk-tool" - a simple test tool that returns a greeting -- **Resource**: "Web Search" - a placeholder resource demonstrating resource handling +| Name | URI | Description | +|--------------|-----------------------|---------------------------------------------------------| +| `Web Search` | `https://search.com/` | A placeholder resource demonstrating resource handling. | diff --git a/samples/notebooks/README.md b/samples/notebooks/README.md new file mode 100644 index 000000000..8db4f3b96 --- /dev/null +++ b/samples/notebooks/README.md @@ -0,0 +1,31 @@ +# MCP Client Notebook + +An interactive Kotlin notebook that demonstrates connecting to a remote MCP server and calling tools. + +## Overview + +This notebook walks through building an MCP client step by step: creating a Ktor HTTP client, +initializing an MCP `Client`, connecting via `StreamableHttpClientTransport`, and interacting with +the server (ping, list tools, call tools). It connects to the public +[Microsoft Learn MCP Server](https://learn.microsoft.com/api/mcp) as an example. + +## Prerequisites + +- [Kotlin Jupyter kernel](https://github.com/Kotlin/kotlin-jupyter) **or** IntelliJ IDEA with the + [Kotlin Notebook plugin](https://plugins.jetbrains.com/plugin/16340-kotlin-notebook) +- Internet access (the notebook connects to an external MCP server) + +## How to Run + +1. Open `McpClient.ipynb` in IntelliJ IDEA (with Kotlin Notebook plugin) or in Jupyter with the + Kotlin kernel installed. +2. Run cells sequentially from top to bottom. + +## What It Demonstrates + +- Adding MCP SDK dependencies in a notebook environment +- Creating a Ktor `HttpClient` with SSE and logging plugins +- Creating an MCP `Client` and connecting via `StreamableHttpClientTransport` +- Sending a `ping` request +- Listing available tools from the remote server +- Calling a tool (`microsoft_docs_search`) and displaying results \ No newline at end of file diff --git a/samples/simple-streamable-server/README.md b/samples/simple-streamable-server/README.md new file mode 100644 index 000000000..27856d724 --- /dev/null +++ b/samples/simple-streamable-server/README.md @@ -0,0 +1,75 @@ +# Simple Streamable HTTP Server + +A minimal MCP server using the recommended Streamable HTTP transport with optional Bearer token authentication. + +## Overview + +This sample demonstrates a Streamable HTTP MCP server built with Ktor. It exposes tools, a prompt +template, and a resource over HTTP, and optionally supports Bearer token authentication. The +`multi-greet` tool showcases server-to-client logging notifications with streaming delays. + +## Prerequisites + +- JDK 17+ + +## Build & Run + +### Without authentication + +```shell +./gradlew run +``` + +The server starts on `http://localhost:3001/mcp` by default. + +Connect with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): + +```shell +npx @modelcontextprotocol/inspector +``` + +In the Inspector UI, select **Streamable HTTP** transport and enter `http://localhost:3001/mcp`. + +Pass a port number as an argument to change it: + +```shell +./gradlew run --args="8080" +``` + +### With authentication + +```shell +MCP_AUTH_TOKEN=my-secret ./gradlew run --args="--auth" +``` + +When `--auth` is passed, clients must include an `Authorization: Bearer ` header. +The token is read from the `MCP_AUTH_TOKEN` environment variable (required when `--auth` is used). + +### Authentication caveats + +This sample is intended **for demonstration only**. In production you should: + +- Use a proper identity provider instead of a static token. +- Restrict CORS origins to your deployment domain — the sample allows all origins. +- Serve the endpoint over HTTPS. + +## MCP Capabilities + +### Tools + +| Name | Description | +|------|-------------| +| `greet` | Returns a greeting for a given `name`. | +| `multi-greet` | Sends logging notifications between delayed greetings, demonstrating streaming. | + +### Prompts + +| Name | Description | +|------|-------------| +| `greeting-template` | Generates a friendly greeting message for a given `name`. | + +### Resources + +| Name | URI | Description | +|------|-----|-------------| +| `Default Greeting` | `https://example.com/greetings/default` | Returns a static "Hello, world!" text. | \ No newline at end of file diff --git a/samples/simple-streamable-server/build.gradle.kts b/samples/simple-streamable-server/build.gradle.kts new file mode 100644 index 000000000..8cb908e1c --- /dev/null +++ b/samples/simple-streamable-server/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.shadow) + application +} + +group = "org.example" +version = "0.1.0" + +application { + mainClass.set("io.modelcontextprotocol.sample.server.MainKt") +} + +dependencies { + implementation(dependencies.platform(libs.ktor.bom)) + implementation(libs.mcp.kotlin.server) + implementation(libs.ktor.server.netty) + implementation(libs.ktor.server.auth) + implementation(libs.ktor.server.cors) + implementation(libs.ktor.server.content.negotiation) + implementation(libs.ktor.server.sse) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.slf4j.simple) +} + +kotlin { + jvmToolchain(17) +} diff --git a/samples/simple-streamable-server/gradle.properties b/samples/simple-streamable-server/gradle.properties new file mode 100644 index 000000000..32cead691 --- /dev/null +++ b/samples/simple-streamable-server/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.configuration-cache=true +org.gradle.parallel=true +org.gradle.caching=true + +#mcp.kotlin.overrideVersion=0.0.1-SNAPSHOT diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml new file mode 100644 index 000000000..7240ff23c --- /dev/null +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -0,0 +1,22 @@ +[versions] +kotlin = "2.2.21" +ktor = "3.2.3" +mcp-kotlin = "0.9.0" +slf4j = "2.0.17" +shadow = "9.2.2" + +[libraries] +ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } +ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty" } +ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors" } +ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation" } +ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json" } +ktor-server-auth = { group = "io.ktor", name = "ktor-server-auth" } +ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse" } +mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } +slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } diff --git a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.jar b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..61285a659d17295f1de7c53e24fdf13ad755c379 GIT binary patch literal 46175 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

pVX@HRij*jw$g4?}Pn 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 literal 0 HcmV?d00001 diff --git a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..37f78a6af --- /dev/null +++ b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/simple-streamable-server/gradlew b/samples/simple-streamable-server/gradlew new file mode 100755 index 000000000..adff685a0 --- /dev/null +++ b/samples/simple-streamable-server/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 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 + + + +# 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" ) + + 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" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# 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/simple-streamable-server/gradlew.bat b/samples/simple-streamable-server/gradlew.bat new file mode 100644 index 000000000..c4bdd3ab8 --- /dev/null +++ b/samples/simple-streamable-server/gradlew.bat @@ -0,0 +1,93 @@ +@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 + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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/simple-streamable-server/settings.gradle.kts b/samples/simple-streamable-server/settings.gradle.kts new file mode 100644 index 000000000..3c36808b9 --- /dev/null +++ b/samples/simple-streamable-server/settings.gradle.kts @@ -0,0 +1,25 @@ +rootProject.name = "simple-streamable-server" + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +dependencyResolutionManagement { + repositories { + mavenLocal() + mavenCentral() + } + + versionCatalogs { + create("libs") { + val mcpKotlinVersion = providers.gradleProperty( + "mcp.kotlin.overrideVersion", + ).orNull + if (mcpKotlinVersion != null) { + logger.lifecycle("Using the override version $mcpKotlinVersion of MCP Kotlin SDK") + version("mcp-kotlin", mcpKotlinVersion) + } + } + } +} diff --git a/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt b/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt new file mode 100644 index 000000000..161edc7c7 --- /dev/null +++ b/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt @@ -0,0 +1,28 @@ +package io.modelcontextprotocol.sample.server + +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty + +fun main(vararg args: String) { + val authEnabled = args.any { it == "--auth" } + val port = args.firstOrNull { it != "--auth" }?.toIntOrNull() ?: 3001 + val authToken = if (authEnabled) { + val envToken = System.getenv("MCP_AUTH_TOKEN") + requireNotNull(envToken) { + "MCP_AUTH_TOKEN environment variable must be set when using --auth" + } + envToken + } else { + null + } + + println("Starting MCP Streamable HTTP server on port $port") + println("Use MCP inspector to connect to http://localhost:$port/mcp") + if (authToken != null) { + println("Bearer auth enabled (token sourced from MCP_AUTH_TOKEN)") + } + + embeddedServer(Netty, host = "127.0.0.1", port = port) { + configureServer(authToken) + }.start(wait = true) +} diff --git a/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt b/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt new file mode 100644 index 000000000..cdffdae7f --- /dev/null +++ b/samples/simple-streamable-server/src/main/kotlin/io/modelcontextprotocol/sample/server/server.kt @@ -0,0 +1,297 @@ +package io.modelcontextprotocol.sample.server + +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.install +import io.ktor.server.auth.Authentication +import io.ktor.server.auth.authenticate +import io.ktor.server.auth.bearer +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.plugins.cors.routing.CORS +import io.ktor.server.request.header +import io.ktor.server.response.respond +import io.ktor.server.routing.delete +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.server.sse.sse +import io.ktor.util.collections.ConcurrentMap +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.StreamableHttpServerTransport +import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import kotlinx.coroutines.delay +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject +import kotlin.time.Duration.Companion.milliseconds + +private const val MCP_SESSION_ID_HEADER = "mcp-session-id" + +fun Application.configureServer(authToken: String? = null) { + installCors(authEnabled = authToken != null) + install(ContentNegotiation) { + json(McpJson) + } + + if (authToken == null) { + mcpStreamableHttp { + createMcpServer() + } + } else { + configureAuthenticatedMcp(authToken) + } +} + +private fun Application.configureAuthenticatedMcp(authToken: String) { + install(SSE) + install(Authentication) { + bearer("mcp-bearer") { + authenticate { credential -> + if (credential.token == authToken) { + io.ktor.server.auth.UserIdPrincipal("mcp-client") + } else { + null + } + } + } + } + + val transports = ConcurrentMap() + + routing { + authenticate("mcp-bearer") { + route("/mcp") { + sse { + val transport = findTransport(call, transports) ?: return@sse + transport.handleRequest(this, call) + } + + post { + val transport = getOrCreateTransport(call, transports) ?: return@post + transport.handleRequest(null, call) + } + + delete { + val transport = findTransport(call, transports) ?: return@delete + transport.handleRequest(null, call) + } + } + } + } +} + +private suspend fun findTransport( + call: ApplicationCall, + transports: ConcurrentMap, +): StreamableHttpServerTransport? { + val sessionId = call.request.header(MCP_SESSION_ID_HEADER) + if (sessionId.isNullOrEmpty()) { + call.respond(HttpStatusCode.BadRequest, "Bad Request: No valid session ID provided") + return null + } + val transport = transports[sessionId] + if (transport == null) { + call.respond(HttpStatusCode.NotFound, "Session not found") + return null + } + return transport +} + +private suspend fun getOrCreateTransport( + call: ApplicationCall, + transports: ConcurrentMap, +): StreamableHttpServerTransport? { + val sessionId = call.request.header(MCP_SESSION_ID_HEADER) + if (sessionId != null) { + val transport = transports[sessionId] + if (transport == null) { + call.respond(HttpStatusCode.NotFound, "Session not found") + } + return transport + } + + val configuration = StreamableHttpServerTransport.Configuration( + enableJsonResponse = true, + ) + val transport = StreamableHttpServerTransport(configuration) + + transport.setOnSessionInitialized { initializedSessionId -> + transports[initializedSessionId] = transport + } + transport.setOnSessionClosed { closedSessionId -> + transports.remove(closedSessionId) + } + + val server = createMcpServer() + server.onClose { + transport.sessionId?.let { transports.remove(it) } + } + server.createSession(transport) + + return transport +} + +private fun Application.installCors(authEnabled: Boolean = false) { + install(CORS) { + anyHost() // Don't do this in production if possible. Try to limit it. + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowMethod(HttpMethod.Delete) + allowNonSimpleContentTypes = true + allowHeader("Mcp-Session-Id") + allowHeader("Mcp-Protocol-Version") + exposeHeader("Mcp-Session-Id") + exposeHeader("Mcp-Protocol-Version") + if (authEnabled) { + allowHeader(HttpHeaders.Authorization) + } + } +} + +private fun createMcpServer(): Server { + val server = Server( + Implementation( + name = "simple-streamable-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), + logging = ServerCapabilities.Logging, + ), + ), + ) + + // Tool: greet + server.addTool( + name = "greet", + description = "A simple greeting tool", + inputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("name") { + put("type", "string") + put("description", "Name to greet") + } + }, + required = listOf("name"), + ), + ) { request -> + val name = request.arguments?.get("name")?.jsonPrimitive?.content ?: "World" + CallToolResult(content = listOf(TextContent("Hello, $name!"))) + } + + // Tool: multi-greet (demonstrates logging notifications) + server.addTool( + name = "multi-greet", + description = "A tool that sends different greetings with delays between them", + inputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("name") { + put("type", "string") + put("description", "Name to greet") + } + }, + required = listOf("name"), + ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = false), + ) { request -> + val name = request.arguments?.get("name")?.jsonPrimitive?.content ?: "World" + + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Debug, + data = JsonPrimitive("Starting multi-greet for $name") + ) + ) + ) + delay(1000.milliseconds) + + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive("Sending first greeting to $name") + ) + ) + ) + delay(1000.milliseconds) + + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive("Sending second greeting to $name") + ) + ) + ) + + CallToolResult(content = listOf(TextContent("Good morning, $name!"))) + } + + // Prompt: greeting-template + 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 -> + val name = request.arguments?.get("name") ?: "World" + GetPromptResult( + messages = listOf( + PromptMessage( + role = Role.User, + content = TextContent("Please greet $name in a friendly manner."), + ), + ), + ) + } + + // Resource: greeting-resource + 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 +} diff --git a/samples/weather-stdio-server/README.md b/samples/weather-stdio-server/README.md index 0e8efc0ee..4c13040c8 100644 --- a/samples/weather-stdio-server/README.md +++ b/samples/weather-stdio-server/README.md @@ -1,140 +1,67 @@ -# Kotlin MCP Weather STDIO Server +# Weather STDIO Server -This project demonstrates how to build a Model Context Protocol (MCP) server in Kotlin that provides weather-related -tools by consuming the National Weather Service (weather.gov) API. The server uses STDIO as the transport layer and -leverages the Kotlin MCP SDK to expose weather forecast and alert tools. +A minimal MCP server that exposes weather forecast and alert tools using the National Weather +Service API over STDIO transport. -For more information about the MCP SDK and protocol, please refer to -the [MCP documentation](https://modelcontextprotocol.io/introduction). +## Overview -## Prerequisites - -- Java 17 or later -- Gradle (or the Gradle wrapper provided with the project) -- Basic understanding of MCP concepts -- Basic understanding of Kotlin and Kotlin ecosystems (sush as kotlinx-serialization, coroutines, ktor) - -## MCP Weather Server +This sample shows how to build a STDIO-based MCP server in Kotlin. It registers two tools that +query the [weather.gov](https://www.weather.gov/) API — one for weather forecasts by +latitude/longitude and one for active alerts by US state. Because it uses STDIO, the server is +launched as a subprocess by an MCP client or a desktop application like Claude Desktop. -The project provides: +## Prerequisites -- A lightweight MCP server built with Kotlin. -- STDIO transport layer implementation for server-client communication. -- Two weather tools: - - **Weather Forecast Tool** — returns details such as temperature, wind information, and a detailed forecast for a - given latitude/longitude. - - **Weather Alerts Tool** — returns active weather alerts for a given US state. +- JDK 17+ +- Internet access (the server calls the weather.gov API at runtime) -## Building and running +## Build & Run -Use the Gradle wrapper to build the application. In a terminal run: +Run the server (it communicates via stdin/stdout): ```shell -./gradlew clean build +./gradlew run ``` -To run the server: +### MCP Inspector -```shell -java -jar build/libs/weather-stdio-server-0.1.0-all.jar -``` - -> [!NOTE] -> The server uses STDIO transport, so it is typically launched in an environment where the client connects via standard -> input/output. - -## Tool Implementation - -The project registers two MCP tools using the Kotlin MCP SDK. Below is an overview of the core tool implementations: - -### 1. Weather Forecast Tool - -This tool fetches the weather forecast for a specific latitude and longitude using the `weather.gov` API. - -Example tool registration in Kotlin: - -```kotlin -server.addTool( - name = "get_forecast", - description = """ - Get weather forecast for a specific latitude/longitude - """.trimIndent(), - inputSchema = Tool.Input( - properties = JsonObject( - mapOf( - "latitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))), - "longitude" to JsonObject(mapOf("type" to JsonPrimitive("number"))), - ) - ), - required = listOf("latitude", "longitude") - ) -) { request -> - // Implementation tool -} -``` +Build the fat JAR first, then connect with the +[MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): -### 2. Weather Alerts Tool - -This tool retrieves active weather alerts for a US state. - -Example tool registration in Kotlin: - -```kotlin -server.addTool( - name = "get_alerts", - description = """ - Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY) - """.trimIndent(), - inputSchema = Tool.Input( - properties = JsonObject( - mapOf( - "state" to JsonObject( - mapOf( - "type" to JsonPrimitive("string"), - "description" to JsonPrimitive("Two-letter US state code (e.g. CA, NY)") - ) - ), - ) - ), - required = listOf("state") - ) -) { request -> - // Implementation tool -} +```shell +./gradlew build +npx @modelcontextprotocol/inspector -- java -jar samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar ``` -## Client Integration - -### Kotlin Client Example - -Since the server uses STDIO for transport, the client typically connects via standard input/output streams. A sample -client implementation can be found in the tests, demonstrating how to send tool requests and process responses. +### Claude Desktop integration -### Claude for Desktop - -To integrate with Claude Desktop, add the following configuration to your Claude Desktop settings: +Add the following to your Claude Desktop configuration: ```json { - "mcpServers": { - "weather": { - "command": "java", - "args": [ - "-jar", - "/absolute/path/to/.jar" - ] + "mcpServers": { + "weather": { + "command": "java", + "args": [ + "-jar", + "/absolute/path/to/samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar" + ] + } } - } } ``` -> [!NOTE] -> Replace `/absolute/path/to/.jar` with the actual absolute path to your built jar file. +## MCP Capabilities + +### Tools + +| Name | Description | +|----------------|------------------------------------------------------------------------------------------| +| `get_forecast` | Returns weather forecast for a given `latitude` / `longitude` using the weather.gov API. | +| `get_alerts` | Returns active weather alerts for a two-letter US `state` code (e.g. `CA`, `NY`). | ## Additional Resources -- [MCP Specification](https://spec.modelcontextprotocol.io/) +- [MCP Specification](https://modelcontextprotocol.io/specification/latest) - [Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk) -- [Ktor Client Documentation](https://ktor.io/docs/welcome.html) -- [Kotlinx Serialization](https://kotlinlang.org/docs/serialization.html) - +- [National Weather Service API](https://www.weather.gov/documentation/services-web-api) From 338f94804f0df9925d4ef48752266f7bf0e1c40b Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 10 Mar 2026 20:05:44 +0100 Subject: [PATCH 016/187] feat: add tool execution properties to Tool (#567) closes #543 ## How Has This Been Tested? unit ## Breaking Changes binary compatibility in the Tool class ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --------- Co-authored-by: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> --- kotlin-sdk-core/api/kotlin-sdk-core.api | 55 ++++++++++- .../modelcontextprotocol/kotlin/sdk/types.kt | 2 +- .../kotlin/sdk/types/tools.kt | 35 +++++++ .../kotlin/sdk/types/ToolsTest.kt | 96 +++++++++++++++++++ kotlin-sdk-server/api/kotlin-sdk-server.api | 4 +- .../kotlin/sdk/server/Server.kt | 4 + 6 files changed, 188 insertions(+), 8 deletions(-) diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index 0607df500..af22323e2 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -4335,6 +4335,20 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestPara public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskSupport : java/lang/Enum { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport$Companion; + public static final field Forbidden Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; + public static final field Optional Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; + public static final field Required Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskSupport$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/TextContent : io/modelcontextprotocol/kotlin/sdk/types/MediaContent { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TextContent$Companion; public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)V @@ -4413,8 +4427,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/TextResourceContents public final class io/modelcontextprotocol/kotlin/sdk/types/Tool : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Tool$Companion; - public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; public final fun component3 ()Ljava/lang/String; @@ -4422,12 +4436,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Tool : io/modelconte public final fun component5 ()Ljava/lang/String; public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations; public final fun component7 ()Ljava/util/List; - public final fun component8 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/Tool; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Tool; + public final fun component8 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution; + public final fun component9 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/Tool; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Tool; public fun equals (Ljava/lang/Object;)Z public final fun getAnnotations ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations; public final fun getDescription ()Ljava/lang/String; + public final fun getExecution ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution; public final fun getIcons ()Ljava/util/List; public final fun getInputSchema ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; public fun getMeta ()Lkotlinx/serialization/json/JsonObject; @@ -4491,6 +4507,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations$Comp public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolExecution { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution; + public fun equals (Ljava/lang/Object;)Z + public final fun getTaskSupport ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolExecution$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution; + 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/types/ToolExecution;)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/types/ToolExecution$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification : io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification$Companion; public fun ()V diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt index 1b43482d0..3b2b2d2b6 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt @@ -1618,7 +1618,7 @@ public fun Tool( title, annotations, icons, - _meta, + meta = _meta, ) /** diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt index b7aade56f..c121f9d27 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt @@ -55,6 +55,8 @@ public fun CallToolResult.Companion.error(content: String, meta: JsonObject? = n * @property icons Optional set of sized icons that clients can display in their user interface. * Clients MUST support at least PNG and JPEG formats. * Clients SHOULD also support SVG and WebP formats. + * @property execution Optional execution-related properties for this tool, + * such as task-augmented execution support. * @property meta Optional metadata for this tool. */ @Serializable @@ -66,6 +68,7 @@ public data class Tool( val title: String? = null, val annotations: ToolAnnotations? = null, val icons: List? = null, + val execution: ToolExecution? = null, @SerialName("_meta") override val meta: JsonObject? = null, ) : WithMeta @@ -92,6 +95,38 @@ public data class ToolSchema( val type: String = "object" } +/** + * Execution-related properties for a tool. + * + * @property taskSupport Indicates whether this tool supports task-augmented execution. + * This allows clients to handle long-running operations through polling the task system. + * If omitted (`null`), consumers should treat it as [TaskSupport.Forbidden] per the spec. + */ +@Serializable +public data class ToolExecution(val taskSupport: TaskSupport? = null) + +/** + * Indicates whether a tool supports task-augmented execution. + * + * - [Forbidden]: Tool does not support task-augmented execution (default when absent). + * - [Optional]: Tool may support task-augmented execution. + * - [Required]: Tool requires task-augmented execution. + */ +@Serializable +public enum class TaskSupport { + /** Tool does not support task-augmented execution. */ + @SerialName("forbidden") + Forbidden, + + /** Tool may support task-augmented execution. */ + @SerialName("optional") + Optional, + + /** Tool requires task-augmented execution. */ + @SerialName("required") + Required, +} + /** * Additional properties describing a Tool to clients. * diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt index 4663fbdf9..543e12d16 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt @@ -475,6 +475,102 @@ class ToolsTest { assertEquals(null, result.structuredContent) } + @Test + fun `should serialize ToolExecution with taskSupport`() { + val execution = ToolExecution(taskSupport = TaskSupport.Required) + + verifySerialization( + execution, + McpJson, + """ + { + "taskSupport": "required" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ToolExecution without taskSupport`() { + val execution = ToolExecution() + + verifySerialization( + execution, + McpJson, + "{}", + ) + } + + @Test + fun `should deserialize ToolExecution with taskSupport`() { + val json = """ + { + "taskSupport": "optional" + } + """.trimIndent() + + val execution = verifyDeserialization(McpJson, json) + + assertEquals(TaskSupport.Optional, execution.taskSupport) + } + + @Test + fun `should deserialize ToolExecution without taskSupport`() { + val json = "{}" + + val execution = verifyDeserialization(McpJson, json) + + assertEquals(null, execution.taskSupport) + } + + @Test + fun `should serialize Tool with execution`() { + val tool = Tool( + name = "long-running-task", + inputSchema = ToolSchema(), + description = "A tool that supports task-augmented execution", + execution = ToolExecution(taskSupport = TaskSupport.Required), + ) + + verifySerialization( + tool, + McpJson, + """ + { + "name": "long-running-task", + "inputSchema": { + "type": "object" + }, + "description": "A tool that supports task-augmented execution", + "execution": { + "taskSupport": "required" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize Tool with execution`() { + val json = """ + { + "name": "long-running-task", + "inputSchema": { + "type": "object" + }, + "execution": { + "taskSupport": "forbidden" + } + } + """.trimIndent() + + val tool = verifyDeserialization(McpJson, json) + + assertEquals("long-running-task", tool.name) + assertNotNull(tool.execution) + assertEquals(TaskSupport.Forbidden, tool.execution.taskSupport) + } + @Test fun `should serialize tool schema type even when defaults disabled`() { val tool = weatherTool() diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index c0b0199dd..a6f3b6d72 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -99,8 +99,8 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { 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/Function3;ILjava/lang/Object;)V public final fun addResources (Ljava/util/List;)V public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function3;)V - public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function3;)V - public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V + public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function3;)V + public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V public final fun addTools (Ljava/util/List;)V public final fun clientConnection (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection; public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index 240170976..7842c2b2e 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -38,6 +38,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.Tool import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations +import io.modelcontextprotocol.kotlin.sdk.types.ToolExecution import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest import kotlinx.coroutines.CancellationException @@ -313,6 +314,7 @@ public open class Server( * @param inputSchema The expected input schema for the tool. * @param outputSchema The optional expected output schema for the tool. * @param toolAnnotations Optional additional tool information. + * @param execution Optional execution-related properties, such as task-augmented execution support. * @param meta Optional metadata as a [JsonObject]. * @param handler A suspend function that handles executing the tool when called by the client. * @throws IllegalStateException If the server does not support tools. @@ -325,6 +327,7 @@ public open class Server( title: String? = null, outputSchema: ToolSchema? = null, toolAnnotations: ToolAnnotations? = null, + execution: ToolExecution? = null, meta: JsonObject? = null, handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult, ) { @@ -335,6 +338,7 @@ public open class Server( description = description, title = title, annotations = toolAnnotations, + execution = execution, meta = meta, ) addTool(tool, handler) From e88b34170cd832fe5f50c8b6da02bb5ffe1e6ab1 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Thu, 12 Mar 2026 14:44:59 +0100 Subject: [PATCH 017/187] feat!: add mcp conformance test infra (#585); add SSE reconnection with retry support (#596) (#585) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a comprehensive conformance test suite for the Kotlin MCP SDK, covering core protocol operations, tool calls, elicitation, resources, prompts, and 20 OAuth/auth scenarios - Conformance server and client implementations - OAuth/auth test scenarios: JWT, authorization code flow, client credentials, PKCE, scope handling, cross-app access, client registration - CI workflow - Baseline file for tracking expected failures - Shell script fixes: - #592 - #593 - #596 ## Remaining known failures (tracked issues, will be fixed directly in `main`) - [x] `tools-call-with-logging`, `tools-call-with-progress`, `tools-call-sampling`, `tools-call-elicitation`, `elicitation-sep1034-defaults`- see #599, - [x] `elicitation-sep1330-enums` - #587 #600 - [x] `initialize` - #588 - [x] `tools_call`, `auth/scope-step-up`, `auth/scope-retry-limit` - #589 - [ ] `elicitation-sep1034-client-defaults` - #414 - [x] `sse-retry` - #590 - [ ] `resources-templates-read` - #591 ## Breaking Changes from #596 - `StreamableHttpClientTransport` and `mcpStreamableHttp`/`mcpStreamableHttpTransport`: old constructors accepting `Duration` timeout are now `@Deprecated` — use the new overloads with `ReconnectionOptions` instead - `StreamableHttpClientTransport.close()` no longer calls `terminateSession()` automatically ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --------- Co-authored-by: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> --- .github/workflows/conformance.yml | 175 +++-- build.gradle.kts | 13 +- conformance-test/.gitignore | 1 + conformance-test/README.md | 130 ++++ conformance-test/build.gradle.kts | 53 +- conformance-test/conformance-baseline.yml | 13 + conformance-test/detekt-baseline.xml | 9 - conformance-test/run-conformance.sh | 194 ++++++ .../sdk/conformance/ConformanceClient.kt | 199 ++++++ .../sdk/conformance/ConformanceCompletions.kt | 15 + .../sdk/conformance/ConformancePrompts.kt | 114 ++++ .../sdk/conformance/ConformanceResources.kt | 123 ++++ .../sdk/conformance/ConformanceServer.kt | 47 ++ .../sdk/conformance/ConformanceTools.kt | 604 ++++++++++++++++++ .../sdk/conformance/InMemoryEventStore.kt | 46 ++ .../sdk/conformance/auth/JWTScenario.kt | 189 ++++++ .../sdk/conformance/auth/authCodeFlow.kt | 154 +++++ .../sdk/conformance/auth/basicScenario.kt | 54 ++ .../conformance/auth/clientRegistration.kt | 73 +++ .../auth/crossAppAccessScenario.kt | 82 +++ .../kotlin/sdk/conformance/auth/discovery.kt | 48 ++ .../kotlin/sdk/conformance/auth/pkce.kt | 30 + .../sdk/conformance/auth/registration.kt | 32 + .../sdk/conformance/auth/scopeHandling.kt | 29 + .../sdk/conformance/auth/tokenExchange.kt | 174 +++++ .../kotlin/sdk/conformance/auth/utils.kt | 120 ++++ .../sdk/conformance/ConformanceClient.kt | 85 --- .../sdk/conformance/ConformanceServer.kt | 426 ------------ .../kotlin/sdk/conformance/ConformanceTest.kt | 343 ---------- .../conformance/WebSocketConformanceClient.kt | 105 --- .../test/resources/simplelogger.properties | 10 - gradle/libs.versions.toml | 1 + kotlin-sdk-client/api/kotlin-sdk-client.api | 18 + .../kotlin/sdk/client/ReconnectionOptions.kt | 44 ++ .../client/StreamableHttpClientTransport.kt | 215 +++++-- .../StreamableHttpMcpKtorClientExtensions.kt | 67 +- .../http/StreamableHttpClientTransportTest.kt | 199 ++++++ .../sdk/client/StreamableHttpClientTest.kt | 27 +- .../kotlin/sdk/types/common.kt | 5 +- .../kotlin/sdk/types/serializers.kt | 18 +- .../kotlin/sdk/types/CommonTypeTest.kt | 3 +- .../server/StreamableHttpServerTransport.kt | 21 +- 42 files changed, 3186 insertions(+), 1122 deletions(-) create mode 100644 conformance-test/.gitignore create mode 100644 conformance-test/README.md create mode 100644 conformance-test/conformance-baseline.yml delete mode 100644 conformance-test/detekt-baseline.xml create mode 100755 conformance-test/run-conformance.sh create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceCompletions.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformancePrompts.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/InMemoryEventStore.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/JWTScenario.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/authCodeFlow.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/basicScenario.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/clientRegistration.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/crossAppAccessScenario.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/discovery.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/pkce.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/scopeHandling.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/tokenExchange.kt create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/utils.kt delete mode 100644 conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt delete mode 100644 conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt delete mode 100644 conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTest.kt delete mode 100644 conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/WebSocketConformanceClient.kt delete mode 100644 conformance-test/src/test/resources/simplelogger.properties create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions.kt diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 1c9ba19ca..b218b4ca1 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -7,67 +7,166 @@ on: push: branches: [ main ] +env: + JAVA_VERSION: '21' + JAVA_DISTRIBUTION: temurin + NODE_VERSION: '22' + + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} # Cancel only when the run is NOT on `main` branch cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: - run-conformance: - runs-on: ${{ matrix.os }} - name: Run Conformance Tests on ${{ matrix.os }} + server: + runs-on: ubuntu-latest + name: Conformance Server Tests + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v6 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + + - name: Build + run: ./gradlew :conformance-test:installDist + + - name: Start server + run: | + MCP_PORT=3001 conformance-test/build/install/conformance-test/bin/conformance-test & + for i in $(seq 1 30); do + if curl -s -o /dev/null http://localhost:3001/mcp; then + echo "Server is ready" + break + fi + sleep 1 + done + + - name: Run conformance tests + uses: modelcontextprotocol/conformance@v0.1.15 + with: + mode: server + url: http://localhost:3001/mcp + suite: active + node-version: ${{ env.NODE_VERSION }} + expected-failures: ./conformance-test/conformance-baseline.yml + + client: + runs-on: ubuntu-latest + name: "Conformance Client Tests: ${{ matrix.scenario }}" timeout-minutes: 20 - env: - JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" strategy: fail-fast: false matrix: - include: - - os: ubuntu-latest - max-workers: 3 - - os: windows-latest - max-workers: 3 - - os: macos-latest - max-workers: 2 + scenario: + - initialize + - tools_call + - elicitation-sep1034-client-defaults + - sse-retry + + steps: + - uses: actions/checkout@v6 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + + - name: Build + run: ./gradlew :conformance-test:installDist + + - name: Run conformance tests + uses: modelcontextprotocol/conformance@v0.1.15 + with: + mode: client + command: conformance-test/build/install/conformance-test/bin/conformance-client + scenario: ${{ matrix.scenario }} + node-version: ${{ env.NODE_VERSION }} + expected-failures: ./conformance-test/conformance-baseline.yml + + auth: + runs-on: ubuntu-latest + name: Conformance Auth Tests + timeout-minutes: 20 steps: - uses: actions/checkout@v6 - - name: Set up JDK 21 + - name: Set up JDK uses: actions/setup-java@v5 with: - java-version: '21' - distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} - - name: Setup Node.js - uses: actions/setup-node@v6 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 with: - node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed + cache-read-only: ${{ github.ref != 'refs/heads/main' }} - - name: Setup Conformance Tests - working-directory: conformance-test - run: |- - npm install -g @modelcontextprotocol/conformance@0.1.8 + - name: Build + run: ./gradlew :conformance-test:installDist + + - name: Run conformance tests + uses: modelcontextprotocol/conformance@v0.1.15 + with: + mode: client + command: conformance-test/build/install/conformance-test/bin/conformance-client + suite: auth + node-version: ${{ env.NODE_VERSION }} + expected-failures: ./conformance-test/conformance-baseline.yml + + auth-scenarios: + runs-on: ubuntu-latest + name: "Conformance Auth Scenario: ${{ matrix.scenario }}" + timeout-minutes: 20 + + strategy: + fail-fast: false + matrix: + scenario: + - auth/client-credentials-jwt + - auth/client-credentials-basic + - auth/cross-app-access-complete-flow + + steps: + - uses: actions/checkout@v6 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 with: - add-job-summary: 'always' cache-read-only: ${{ github.ref != 'refs/heads/main' }} - gradle-home-cache-includes: | - caches - notifications - sdks - ../.konan/** - - - name: Run Conformance Tests - run: |- - ./gradlew :conformance-test:test --no-daemon --max-workers ${{ matrix.max-workers }} - - - name: Upload Conformance Results - if: always() - uses: actions/upload-artifact@v7 + + - name: Build + run: ./gradlew :conformance-test:installDist + + - name: Run conformance tests + uses: modelcontextprotocol/conformance@v0.1.15 with: - name: conformance-results-${{ matrix.os }} - path: conformance-test/results/ + mode: client + command: conformance-test/build/install/conformance-test/bin/conformance-client + scenario: ${{ matrix.scenario }} + node-version: ${{ env.NODE_VERSION }} + expected-failures: ./conformance-test/conformance-baseline.yml diff --git a/build.gradle.kts b/build.gradle.kts index f4ddd4451..72c55539b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,12 +23,15 @@ dependencies { subprojects { apply(plugin = "org.jlleitschuh.gradle.ktlint") apply(plugin = "org.jetbrains.kotlinx.kover") - apply(plugin = "dev.detekt") - detekt { - config = files("$rootDir/config/detekt/detekt.yml") - buildUponDefaultConfig = true - failOnSeverity.set(FailOnSeverity.Error) + if (name != "conformance-test" && name != "docs") { + apply(plugin = "dev.detekt") + + detekt { + config = files("$rootDir/config/detekt/detekt.yml") + buildUponDefaultConfig = true + failOnSeverity.set(FailOnSeverity.Error) + } } } diff --git a/conformance-test/.gitignore b/conformance-test/.gitignore new file mode 100644 index 000000000..6628455c0 --- /dev/null +++ b/conformance-test/.gitignore @@ -0,0 +1 @@ +/results/ diff --git a/conformance-test/README.md b/conformance-test/README.md new file mode 100644 index 000000000..d14fcfef3 --- /dev/null +++ b/conformance-test/README.md @@ -0,0 +1,130 @@ +# MCP Conformance Tests + +Conformance tests for the Kotlin MCP SDK. Uses the external +[`@modelcontextprotocol/conformance`](https://www.npmjs.com/package/@modelcontextprotocol/conformance) +runner (pinned to **0.1.15**) to validate compliance with the MCP specification. + +## Prerequisites + +- **JDK 17+** +- **Node.js 18+** and `npx` (for the conformance runner) +- **curl** (used to poll server readiness) + +## Quick Start + +Run **all** suites (server, client core, client auth) from the project root: + +```bash +./conformance-test/run-conformance.sh all +``` + +## Commands + +``` +./conformance-test/run-conformance.sh [extra-args...] +``` + +| Command | What it does | +|---------------|--------------------------------------------------------------------------------------| +| `list` | [List scenarios available in MCP Conformance Test Framework][list-scenarios-command] | +| `server` | Starts the Ktor conformance server, runs the server test suite against it | +| `client` | Runs the client test suite (`initialize`, `tools_call`, `elicitation`, `sse-retry`) | +| `client-auth` | Runs the client auth test suite (20 OAuth scenarios) | +| `all` | Runs all three suites sequentially | + +Any `[extra-args]` are forwarded to the conformance runner (e.g. `--verbose`). + +## What the Script Does + +1. **Builds** the module via `./gradlew :conformance-test:installDist` +2. For `server` — starts the conformance server on `localhost:3001`, polls until ready +3. Invokes `npx @modelcontextprotocol/conformance@0.1.15` with the appropriate arguments +4. Saves results to `conformance-test/results//` +5. Cleans up the server process on exit +6. Exits non-zero if any suite fails + +## Environment Variables + +| Variable | Default | Description | +|------------|---------|---------------------------------| +| `MCP_PORT` | `3001` | Port for the conformance server | + +## Project Structure + +``` +conformance-test/ +├── run-conformance.sh # Single entry point script +├── conformance-baseline.yml # Expected failures for known SDK limitations +└── src/main/kotlin/.../conformance/ + ├── ConformanceServer.kt # Ktor server entry point (StreamableHTTP, DNS rebinding, EventStore) + ├── ConformanceClient.kt # Scenario-based client entry point (MCP_CONFORMANCE_SCENARIO routing) + ├── ConformanceTools.kt # 18 tool registrations + ├── ConformanceResources.kt # 5 resource registrations (static, binary, template, watched, dynamic) + ├── ConformancePrompts.kt # 5 prompt registrations (simple, args, image, embedded, dynamic) + ├── ConformanceCompletions.kt # completion/complete handler + ├── InMemoryEventStore.kt # EventStore impl for SSE resumability (SEP-1699) + └── auth/ # OAuth client for 20 auth scenarios + ├── registration.kt # Scenario handler registration + ├── utils.kt # Shared utilities: JSON instance, constants, extractOrigin() + ├── discovery.kt # Protected Resource Metadata + AS Metadata discovery + ├── pkce.kt # PKCE code verifier/challenge generation + AS capability check + ├── tokenExchange.kt # Token endpoint interaction (exchange code, error handling) + ├── authCodeFlow.kt # Main Authorization Code flow handler (runAuthClient + interceptor) + ├── scopeHandling.kt # Scope selection strategy + step-up 403 handling + ├── clientRegistration.kt # Client registration logic (pre-reg, CIMD, dynamic) + ├── JWTScenario.kt # Client Credentials JWT scenario + ├── basicScenario.kt # Client Credentials Basic scenario + └── crossAppAccessScenario.kt # Cross-App Access (SEP-990) scenario +``` + +## Test Suites + +### Server Suite + +Tests the conformance server against all server scenarios: + +| Category | Scenarios | +|-------------|-------------------------------------------------------------------------------------------------------------------------------------| +| Lifecycle | initialize, ping | +| Tools | text, image, audio, embedded, multiple, progress, logging, error, sampling, elicitation, dynamic, reconnection, JSON Schema 2020-12 | +| Resources | list, read-text, read-binary, templates, subscribe, dynamic | +| Prompts | simple, with-args, with-image, with-embedded-resource, dynamic | +| Completions | complete | +| Security | DNS rebinding protection | + +### Client Core Suite + +| Scenario | Description | +|---------------------------------------|-----------------------------------------------| +| `initialize` | Connect, list tools, close | +| `tools_call` | Connect, call `add_numbers(a=5, b=3)`, close | +| `elicitation-sep1034-client-defaults` | Elicitation with `applyDefaults` capability | +| `sse-retry` | Call `test_reconnection`, verify reconnection | + +### Client Auth Suite + +17 OAuth Authorization Code scenarios + 2 Client Credentials scenarios (`jwt`, `basic`) + 1 Cross-App Access scenario = 20 total. + +> [!NOTE] +> Auth scenarios are implemented using Ktor's `HttpClient` plugins (`HttpSend` interceptor, +> `ktor-client-auth`) as a standalone OAuth client. They do not use the SDK's built-in auth support. + +## Known SDK Limitations + +8 scenarios are expected to fail due to current SDK limitations (tracked in [ +`conformance-baseline.yml`](conformance-baseline.yml). + +| Scenario | Suite | Root Cause | +|---------------------------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| `tools-call-with-logging` | server | Notifications from tool handlers have no `relatedRequestId`; transport routes them to the standalone SSE stream instead of the request-specific stream | +| `tools-call-with-progress` | server | *(same as above)* | +| `tools-call-sampling` | server | *(same as above)* | +| `tools-call-elicitation` | server | *(same as above)* | +| `elicitation-sep1034-defaults` | server | *(same as above)* | +| `elicitation-sep1330-enums` | server | *(same as above)* | +| `resources-templates-read` | server | SDK does not implement `addResourceTemplate()` with URI pattern matching; resources are looked up by exact URI | +| `elicitation-sep1034-client-defaults` | client | SDK does not fill in `default` values from the elicitation request schema before sending the response | + +These failures reveal SDK gaps and are intentionally not fixed in this module. + +[list-scenarios-command]: https://github.com/modelcontextprotocol/conformance/tree/main?tab=readme-ov-file#list-available-scenarios diff --git a/conformance-test/build.gradle.kts b/conformance-test/build.gradle.kts index 1877d0974..a96f4fb56 100644 --- a/conformance-test/build.gradle.kts +++ b/conformance-test/build.gradle.kts @@ -1,39 +1,34 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat - plugins { kotlin("jvm") + application } -dependencies { - testImplementation(project(":kotlin-sdk")) - testImplementation(project(":test-utils")) - testImplementation(kotlin("test")) - testImplementation(libs.kotlin.logging) - testImplementation(libs.ktor.client.cio) - testImplementation(libs.ktor.server.cio) - testImplementation(libs.ktor.server.websockets) - testRuntimeOnly(libs.slf4j.simple) +application { + mainClass.set("io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceServerKt") } -tasks.test { - useJUnitPlatform() +tasks.register("conformanceClientScripts") { + mainClass.set("io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceClientKt") + applicationName = "conformance-client" + outputDir = tasks.named("startScripts").get().outputDir + classpath = tasks.named("jar").get().outputs.files + configurations.named("runtimeClasspath").get() +} - testLogging { - events("passed", "skipped", "failed") - showStandardStreams = true - showExceptions = true - showCauses = true - showStackTraces = true - exceptionFormat = TestExceptionFormat.FULL - } +tasks.named("installDist") { + dependsOn("conformanceClientScripts") +} - doFirst { - systemProperty("test.classpath", classpath.asPath) +tasks.named("clean") { + delete("results") +} - println("\n" + "=".repeat(60)) - println("MCP CONFORMANCE TESTS") - println("=".repeat(60)) - println("These tests validate compliance with the MCP specification.") - println("=".repeat(60) + "\n") - } +dependencies { + implementation(project(":kotlin-sdk")) + implementation(libs.ktor.server.cio) + implementation(libs.ktor.server.content.negotiation) + implementation(libs.ktor.serialization) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.auth) + implementation(libs.kotlin.logging) + runtimeOnly(libs.slf4j.simple) } diff --git a/conformance-test/conformance-baseline.yml b/conformance-test/conformance-baseline.yml new file mode 100644 index 000000000..cc06a389b --- /dev/null +++ b/conformance-test/conformance-baseline.yml @@ -0,0 +1,13 @@ +# Conformance test baseline - expected failures +# Add entries here as tests are identified as known SDK limitations +server: + - tools-call-with-logging + - tools-call-with-progress + - tools-call-sampling + - tools-call-elicitation + - elicitation-sep1034-defaults + - elicitation-sep1330-enums + - resources-templates-read + +client: + - elicitation-sep1034-client-defaults diff --git a/conformance-test/detekt-baseline.xml b/conformance-test/detekt-baseline.xml deleted file mode 100644 index fe120f523..000000000 --- a/conformance-test/detekt-baseline.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - BracesOnWhenStatements:ConformanceServer.kt:HttpServerTransport$when - BracesOnWhenStatements:ConformanceTest.kt:ConformanceTest$when - ForbiddenComment:ConformanceTest.kt:ConformanceTest.Companion$// TODO: Fix - - diff --git a/conformance-test/run-conformance.sh b/conformance-test/run-conformance.sh new file mode 100755 index 000000000..8f15ac0d1 --- /dev/null +++ b/conformance-test/run-conformance.sh @@ -0,0 +1,194 @@ +#!/bin/bash +# Script to run MCP conformance tests for the Kotlin SDK. +# +# Usage: ./conformance-test/run-conformance.sh [extra-args...] +# Commands: server | client | client-auth | all + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1; pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." || exit 1; pwd)" + +CONFORMANCE_VERSION="0.1.15" +PORT="${MCP_PORT:-3001}" +SERVER_URL="http://localhost:${PORT}/mcp" +RESULTS_DIR="$SCRIPT_DIR/results" +SERVER_DIST="$SCRIPT_DIR/build/install/conformance-test/bin/conformance-test" +CLIENT_DIST="$SCRIPT_DIR/build/install/conformance-test/bin/conformance-client" + +SERVER_PID="" + +# shellcheck disable=SC2317 +cleanup() { + if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then + echo "Stopping server (PID: $SERVER_PID)..." + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +build() { + echo "Building conformance-test distributions..." + cd "$PROJECT_ROOT" || return 1 + ./gradlew :conformance-test:installDist --quiet + cd "$SCRIPT_DIR" || return 1 + echo "Build complete." +} + +start_server() { + echo "Starting conformance server on port $PORT..." + MCP_PORT="$PORT" "$SERVER_DIST" & + SERVER_PID=$! + + echo "Waiting for server to be ready..." + local retries=0 + local max_retries=30 + while ! curl -sf "$SERVER_URL" > /dev/null 2>&1; do + retries=$((retries + 1)) + if [ "$retries" -ge "$max_retries" ]; then + echo "ERROR: Server failed to start after $max_retries attempts" + return 1 + fi + sleep 0.5 + done + echo "Server is ready (PID: $SERVER_PID)." +} + +stop_server() { + if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then + echo "Stopping server (PID: $SERVER_PID)..." + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + SERVER_PID="" + fi +} + +run_list_scenarios() { + local output_dir="$RESULTS_DIR/list" + mkdir -p "$output_dir" + echo "" + echo "==========================================" + echo " List Available Scenarios" + echo "==========================================" + local rc=0 + npx "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION" list \ + "$@" > "$output_dir/scenarios.txt" || rc=$? + + cat "$output_dir/scenarios.txt" + return $rc +} + +run_server_suite() { + local output_dir="$RESULTS_DIR/server" + mkdir -p "$output_dir" + echo "" + echo "==========================================" + echo " Running SERVER conformance tests" + echo "==========================================" + start_server || return 1 + local rc=0 + npx "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION" server \ + --url "$SERVER_URL" \ + --output-dir "$output_dir" \ + --expected-failures "$SCRIPT_DIR/conformance-baseline.yml" \ + "$@" || rc=$? + stop_server + return $rc +} + +run_client_suite() { + local output_dir="$RESULTS_DIR/client" + mkdir -p "$output_dir" + echo "" + echo "==========================================" + echo " Running CLIENT conformance tests" + echo "==========================================" + local scenarios=("initialize" "tools_call" "elicitation-sep1034-client-defaults" "sse-retry") + local rc=0 + for scenario in "${scenarios[@]}"; do + npx "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION" client \ + --command "$CLIENT_DIST" \ + --scenario "$scenario" \ + --output-dir "$output_dir" \ + --expected-failures "$SCRIPT_DIR/conformance-baseline.yml" \ + "$@" || rc=$? + done + return $rc +} + +run_client_auth_suite() { + local output_dir="$RESULTS_DIR/client-auth" + mkdir -p "$output_dir" + echo "" + echo "==========================================" + echo " Running CLIENT (auth) conformance tests" + echo "==========================================" + local rc=0 + npx "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION" client \ + --command "$CLIENT_DIST" \ + --suite auth \ + --output-dir "$output_dir" \ + --expected-failures "$SCRIPT_DIR/conformance-baseline.yml" \ + "$@" || rc=$? + + local extra_scenarios=("auth/client-credentials-jwt" "auth/client-credentials-basic" "auth/cross-app-access-complete-flow") + for scenario in "${extra_scenarios[@]}"; do + npx "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION" client \ + --command "$CLIENT_DIST" \ + --scenario "$scenario" \ + --output-dir "$output_dir" \ + --expected-failures "$SCRIPT_DIR/conformance-baseline.yml" \ + "$@" || rc=$? + done + return $rc +} + +# ============================================================================ +# Main +# ============================================================================ + +COMMAND="${1:-}" +shift 2>/dev/null || true + +if [ -z "$COMMAND" ]; then + echo "Usage: $0 [extra-args...]" + echo "Commands: list | server | client | client-auth | all" + exit 1 +fi + +build + +EXIT_CODE=0 + +case "$COMMAND" in + list) + run_list_scenarios "$@" || EXIT_CODE=1 + ;; + server) + run_server_suite "$@" || EXIT_CODE=1 + ;; + client) + run_client_suite "$@" || EXIT_CODE=1 + ;; + client-auth) + run_client_auth_suite "$@" || EXIT_CODE=1 + ;; + all) + run_server_suite "$@" || EXIT_CODE=1 + run_client_suite "$@" || EXIT_CODE=1 + run_client_auth_suite "$@" || EXIT_CODE=1 + ;; + *) + echo "Unknown command: $COMMAND" + echo "Commands: list | server | client | client-auth | all" + exit 1 + ;; +esac + +echo "" +echo "==========================================" +echo " Results saved to: $RESULTS_DIR" +echo "==========================================" + +exit $EXIT_CODE diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt new file mode 100644 index 000000000..dba24ff2b --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt @@ -0,0 +1,199 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.sse.SSE +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.conformance.auth.registerAuthScenarios +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.system.exitProcess +import kotlin.time.Duration.Companion.seconds + +private val logger = KotlinLogging.logger {} + +typealias ScenarioHandler = suspend (serverUrl: String) -> Unit + +internal val scenarioHandlers = mutableMapOf() + +// ============================================================================ +// Main entry point +// ============================================================================ + +fun main(args: Array) { + val scenarioName = System.getenv("MCP_CONFORMANCE_SCENARIO") + val serverUrl = args.lastOrNull() + + // Register all scenario handlers + registerCoreScenarios() + registerAuthScenarios() + + if (scenarioName == null || serverUrl == null) { + logger.error { "Usage: MCP_CONFORMANCE_SCENARIO= conformance-client " } + logger.error { "\nThe MCP_CONFORMANCE_SCENARIO env var is set automatically by the conformance runner." } + logger.error { "\nAvailable scenarios:" } + for (name in scenarioHandlers.keys.sorted()) { + logger.error { " - $name" } + } + exitProcess(1) + } + + val handler = scenarioHandlers[scenarioName] + if (handler == null) { + logger.error { "Unknown scenario: $scenarioName" } + logger.error { "\nAvailable scenarios:" } + for (name in scenarioHandlers.keys.sorted()) { + logger.error { " - $name" } + } + exitProcess(1) + } + + try { + runBlocking { + handler(serverUrl) + } + exitProcess(0) + } catch (e: Exception) { + logger.error(e) { "Error: ${e.message}" } + exitProcess(1) + } +} + +// ============================================================================ +// Shared HTTP client factory +// ============================================================================ + +private fun createHttpClient(): HttpClient = HttpClient(CIO) { + install(SSE) + install(HttpTimeout) { + requestTimeoutMillis = 30.seconds.inWholeMilliseconds + } +} + +// ============================================================================ +// Basic scenarios (initialize, tools_call) +// ============================================================================ + +private suspend fun runBasicClient(serverUrl: String) { + createHttpClient().use { httpClient -> + val transport = StreamableHttpClientTransport(httpClient, serverUrl) + val client = Client( + clientInfo = Implementation("test-client", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + client.connect(transport) + client.close() + } +} + +private suspend fun runToolsCallClient(serverUrl: String) { + createHttpClient().use { httpClient -> + val transport = StreamableHttpClientTransport(httpClient, serverUrl) + val client = Client( + clientInfo = Implementation("test-client", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + client.connect(transport) + + val tools = client.listTools() + val addTool = tools.tools.find { it.name == "add_numbers" } + if (addTool != null) { + client.callTool( + CallToolRequest( + CallToolRequestParams( + name = "add_numbers", + arguments = buildJsonObject { + put("a", 5) + put("b", 3) + }, + ), + ), + ) + } + + client.close() + } +} + +// ============================================================================ +// Elicitation defaults scenario +// ============================================================================ + +private suspend fun runElicitationDefaultsClient(serverUrl: String) { + createHttpClient().use { httpClient -> + val transport = StreamableHttpClientTransport(httpClient, serverUrl) + val client = Client( + clientInfo = Implementation("elicitation-defaults-test-client", "1.0.0"), + options = ClientOptions( + capabilities = ClientCapabilities( + elicitation = ClientCapabilities.elicitation, + ), + ), + ) + + // Register elicitation handler that returns empty content — SDK should fill in defaults + client.setElicitationHandler { _ -> + ElicitResult( + action = ElicitResult.Action.Accept, + content = JsonObject(emptyMap()), + ) + } + + client.connect(transport) + + val tools = client.listTools() + val testTool = tools.tools.find { it.name == "test_client_elicitation_defaults" } + ?: error("Test tool not found: test_client_elicitation_defaults") + + client.callTool( + CallToolRequest(CallToolRequestParams(name = testTool.name)), + ) + + client.close() + } +} + +// ============================================================================ +// SSE retry scenario +// ============================================================================ + +private suspend fun runSSERetryClient(serverUrl: String) { + createHttpClient().use { httpClient -> + val transport = StreamableHttpClientTransport(httpClient, serverUrl) + val client = Client( + clientInfo = Implementation("sse-retry-test-client", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + client.connect(transport) + + client.listTools() + + client.callTool( + CallToolRequest(CallToolRequestParams(name = "test_reconnection")), + ) + + client.close() + } +} + +// ============================================================================ +// Register core scenarios +// ============================================================================ + +private fun registerCoreScenarios() { + scenarioHandlers["initialize"] = ::runBasicClient + scenarioHandlers["tools_call"] = ::runToolsCallClient + scenarioHandlers["elicitation-sep1034-client-defaults"] = ::runElicitationDefaultsClient + scenarioHandlers["sse-retry"] = ::runSSERetryClient +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceCompletions.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceCompletions.kt new file mode 100644 index 000000000..629713949 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceCompletions.kt @@ -0,0 +1,15 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest +import io.modelcontextprotocol.kotlin.sdk.types.CompleteResult +import io.modelcontextprotocol.kotlin.sdk.types.Method + +fun Server.registerConformanceCompletions() { + onConnect { + val session = sessions.values.lastOrNull() ?: return@onConnect + session.setRequestHandler(Method.Defined.CompletionComplete) { _, _ -> + CompleteResult(CompleteResult.Completion(values = emptyList(), total = 0, hasMore = false)) + } + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformancePrompts.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformancePrompts.kt new file mode 100644 index 000000000..6c591e4fd --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformancePrompts.kt @@ -0,0 +1,114 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.types.EmbeddedResource +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.ImageContent +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.milliseconds + +fun Server.registerConformancePrompts() { + // 1. Simple prompt + addPrompt( + name = "test_simple_prompt", + description = "test_simple_prompt", + ) { + GetPromptResult( + messages = listOf( + PromptMessage(Role.User, TextContent("This is a simple prompt for testing.")), + ), + ) + } + + // 2. Prompt with arguments + addPrompt( + name = "test_prompt_with_arguments", + description = "test_prompt_with_arguments", + arguments = listOf( + PromptArgument(name = "arg1", description = "First test argument", required = true), + PromptArgument(name = "arg2", description = "Second test argument", required = true), + ), + ) { request -> + val arg1 = request.arguments?.get("arg1") ?: "" + val arg2 = request.arguments?.get("arg2") ?: "" + GetPromptResult( + messages = listOf( + PromptMessage( + Role.User, + TextContent("Prompt with arguments: arg1='$arg1', arg2='$arg2'"), + ), + ), + ) + } + + // 3. Prompt with image + addPrompt( + name = "test_prompt_with_image", + description = "test_prompt_with_image", + ) { + GetPromptResult( + messages = listOf( + PromptMessage(Role.User, ImageContent(data = PNG_BASE64, mimeType = "image/png")), + PromptMessage(Role.User, TextContent("Please analyze the image above.")), + ), + ) + } + + // 4. Prompt with embedded resource + addPrompt( + name = "test_prompt_with_embedded_resource", + description = "test_prompt_with_embedded_resource", + arguments = listOf( + PromptArgument(name = "resourceUri", description = "URI of the resource to embed", required = true), + ), + ) { request -> + val resourceUri = request.arguments?.get("resourceUri") ?: "test://embedded-resource" + GetPromptResult( + messages = listOf( + PromptMessage( + Role.User, + EmbeddedResource( + resource = TextResourceContents( + text = "Embedded resource content for testing", + uri = resourceUri, + mimeType = "text/plain", + ), + ), + ), + PromptMessage(Role.User, TextContent("Please process the embedded resource above.")), + ), + ) + } + + // 5. Dynamic prompt + val server = this + addPrompt( + name = "test_dynamic_prompt", + description = "test_dynamic_prompt", + ) { + // Add a temporary prompt, triggering listChanged + server.addPrompt( + name = "test_dynamic_prompt_temp", + description = "Temporary dynamic prompt", + ) { + GetPromptResult( + messages = listOf( + PromptMessage(Role.User, TextContent("Temporary prompt response")), + ), + ) + } + delay(100.milliseconds) + // Remove the temporary prompt, triggering listChanged again + server.removePrompt("test_dynamic_prompt_temp") + GetPromptResult( + messages = listOf( + PromptMessage(Role.User, TextContent("Dynamic prompt executed successfully")), + ), + ) + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt new file mode 100644 index 000000000..a09903771 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt @@ -0,0 +1,123 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.types.BlobResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.milliseconds + +fun Server.registerConformanceResources() { + // 1. Static text resource + addResource( + uri = "test://static-text", + name = "static-text", + description = "A static text resource for testing", + mimeType = "text/plain", + ) { + ReadResourceResult( + listOf( + TextResourceContents( + text = "This is the content of the static text resource.", + uri = "test://static-text", + mimeType = "text/plain", + ), + ), + ) + } + + // 2. Static binary resource + addResource( + uri = "test://static-binary", + name = "static-binary", + description = "A static binary resource for testing", + mimeType = "image/png", + ) { + ReadResourceResult( + listOf( + BlobResourceContents( + blob = PNG_BASE64, + uri = "test://static-binary", + mimeType = "image/png", + ), + ), + ) + } + + // 3. Template resource + // Note: The SDK does not currently support addResourceTemplate(). + // Register as a static resource; template listing is handled separately. + addResource( + uri = "test://template/{id}/data", + name = "template", + description = "A template resource for testing", + mimeType = "application/json", + ) { request -> + ReadResourceResult( + listOf( + TextResourceContents( + text = "content for ${request.uri}", + uri = request.uri, + mimeType = "application/json", + ), + ), + ) + } + + // 4. Watched resource + addResource( + uri = "test://watched-resource", + name = "watched-resource", + description = "A watched resource for testing", + mimeType = "text/plain", + ) { + ReadResourceResult( + listOf( + TextResourceContents( + text = "Watched resource content.", + uri = "test://watched-resource", + mimeType = "text/plain", + ), + ), + ) + } + + // 5. Dynamic resource + val server = this + addResource( + uri = "test://dynamic-resource", + name = "dynamic-resource", + description = "A dynamic resource for testing", + mimeType = "text/plain", + ) { + // Add a temporary resource, triggering listChanged + server.addResource( + uri = "test://dynamic-resource-temp", + name = "dynamic-resource-temp", + description = "Temporary dynamic resource", + mimeType = "text/plain", + ) { + ReadResourceResult( + listOf( + TextResourceContents( + text = "Temporary resource content.", + uri = "test://dynamic-resource-temp", + mimeType = "text/plain", + ), + ), + ) + } + delay(100.milliseconds) + // Remove the temporary resource, triggering listChanged again + server.removeResource("test://dynamic-resource-temp") + ReadResourceResult( + listOf( + TextResourceContents( + text = "Dynamic resource content.", + uri = "test://dynamic-resource", + mimeType = "text/plain", + ), + ), + ) + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt new file mode 100644 index 000000000..b42077529 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt @@ -0,0 +1,47 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.install +import io.ktor.server.cio.CIO +import io.ktor.server.engine.embeddedServer +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities + +fun main() { + val port = System.getenv("MCP_PORT")?.toIntOrNull() ?: 3001 + embeddedServer(CIO, port = port) { + install(ContentNegotiation) { + json(McpJson) + } + mcpStreamableHttp( + enableDnsRebindingProtection = true, + allowedHosts = listOf("localhost", "127.0.0.1", "localhost:$port", "127.0.0.1:$port"), + eventStore = InMemoryEventStore(), + ) { + createConformanceServer() + } + }.start(wait = true) +} + +fun createConformanceServer(): Server = Server( + serverInfo = Implementation("mcp-kotlin-sdk-conformance", "0.1.0"), + options = ServerOptions( + ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = true), + resources = ServerCapabilities.Resources(subscribe = true, listChanged = true), + prompts = ServerCapabilities.Prompts(listChanged = true), + logging = ServerCapabilities.Logging, + completions = ServerCapabilities.Completions, + ), + ), +) { + registerConformanceTools() + registerConformanceResources() + registerConformancePrompts() + registerConformanceCompletions() +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt new file mode 100644 index 000000000..ebe7a5716 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt @@ -0,0 +1,604 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.types.AudioContent +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.EmbeddedResource +import io.modelcontextprotocol.kotlin.sdk.types.ImageContent +import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification +import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import kotlinx.coroutines.delay +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.double +import kotlinx.serialization.json.jsonPrimitive +import kotlin.time.Duration.Companion.milliseconds + +// Minimal 1x1 PNG (base64) +internal const val PNG_BASE64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==" + +// Minimal WAV (base64) +internal const val WAV_BASE64 = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA=" + +@Suppress("LongMethod") +fun Server.registerConformanceTools() { + // 1. Simple text + addTool( + name = "test_simple_text", + description = "test_simple_text", + ) { + CallToolResult(listOf(TextContent("Simple text content"))) + } + + // 2. Image content + addTool( + name = "test_image_content", + description = "test_image_content", + ) { + CallToolResult(listOf(ImageContent(data = PNG_BASE64, mimeType = "image/png"))) + } + + // 3. Audio content + addTool( + name = "test_audio_content", + description = "test_audio_content", + ) { + CallToolResult(listOf(AudioContent(data = WAV_BASE64, mimeType = "audio/wav"))) + } + + // 4. Embedded resource + addTool( + name = "test_embedded_resource", + description = "test_embedded_resource", + ) { + CallToolResult( + listOf( + EmbeddedResource( + resource = TextResourceContents( + text = "This is an embedded resource content.", + uri = "test://embedded-resource", + mimeType = "text/plain", + ), + ), + ), + ) + } + + // 5. Multiple content types + addTool( + name = "test_multiple_content_types", + description = "test_multiple_content_types", + ) { + CallToolResult( + listOf( + TextContent("Simple text content"), + ImageContent(data = PNG_BASE64, mimeType = "image/png"), + EmbeddedResource( + resource = TextResourceContents( + text = "This is an embedded resource content.", + uri = "test://embedded-resource", + mimeType = "text/plain", + ), + ), + ), + ) + } + + // 6. Progress tool + addTool( + name = "test_tool_with_progress", + description = "test_tool_with_progress", + ) { request -> + val progressToken = request.meta?.progressToken + if (progressToken != null) { + notification( + ProgressNotification( + ProgressNotificationParams( + progressToken, + 0.0, + 100.0, + "Completed step 0 of 100", + ), + ), + ) + delay(50.milliseconds) + notification( + ProgressNotification( + ProgressNotificationParams( + progressToken, + 50.0, + 100.0, + "Completed step 50 of 100", + ), + ), + ) + delay(50.milliseconds) + notification( + ProgressNotification( + ProgressNotificationParams( + progressToken, + 100.0, + 100.0, + "Completed step 100 of 100", + ), + ), + ) + } + CallToolResult(listOf(TextContent("Simple text content"))) + } + + // 7. Error handling + addTool( + name = "test_error_handling", + description = "test_error_handling", + ) { + throw Exception("This tool intentionally returns an error for testing") + } + + // 8. Sampling + addTool( + name = "test_sampling", + description = "test_sampling", + inputSchema = ToolSchema( + properties = buildJsonObject { + put("prompt", buildJsonObject { put("type", JsonPrimitive("string")) }) + }, + required = listOf("prompt"), + ), + ) { request -> + val prompt = request.arguments?.get("prompt")?.jsonPrimitive?.content ?: "Hello" + val result = createMessage( + CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 10000, + messages = listOf(SamplingMessage(Role.User, TextContent(prompt))), + ), + ), + ) + CallToolResult(listOf(TextContent(result.content.toString()))) + } + + // 9. Elicitation + addTool( + name = "test_elicitation", + description = "test_elicitation", + inputSchema = ToolSchema( + properties = buildJsonObject { + put("message", buildJsonObject { put("type", JsonPrimitive("string")) }) + }, + required = listOf("message"), + ), + ) { request -> + val message = request.arguments?.get("message")?.jsonPrimitive?.content ?: "Please provide input" + val schema = ElicitRequestParams.RequestedSchema( + properties = buildJsonObject { + put( + "username", + buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("User's response")) + }, + ) + put( + "email", + buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("User's email address")) + }, + ) + }, + required = listOf("username", "email"), + ) + val result = createElicitation(message, schema) + CallToolResult(listOf(TextContent("User response: "))) + } + + // 10. Elicitation SEP1034 (defaults) + addTool( + name = "test_elicitation_sep1034_defaults", + description = "test_elicitation_sep1034_defaults", + ) { + val schema = ElicitRequestParams.RequestedSchema( + properties = buildJsonObject { + put( + "name", + buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("User name")) + put("default", JsonPrimitive("John Doe")) + }, + ) + put( + "age", + buildJsonObject { + put("type", JsonPrimitive("integer")) + put("description", JsonPrimitive("User age")) + put("default", JsonPrimitive(30)) + }, + ) + put( + "score", + buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("User score")) + put("default", JsonPrimitive(95.5)) + }, + ) + put( + "status", + buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("User status")) + put("default", JsonPrimitive("active")) + put( + "enum", + JsonArray( + listOf(JsonPrimitive("active"), JsonPrimitive("inactive"), JsonPrimitive("pending")), + ), + ) + }, + ) + put( + "verified", + buildJsonObject { + put("type", JsonPrimitive("boolean")) + put("description", JsonPrimitive("Verification status")) + put("default", JsonPrimitive(true)) + }, + ) + }, + required = listOf("name", "age", "score", "status", "verified"), + ) + val result = createElicitation( + "Please review and update the form fields with defaults", + schema, + ) + CallToolResult(listOf(TextContent(result.content.toString()))) + } + + // 11. Elicitation SEP1330 enums + addTool( + name = "test_elicitation_sep1330_enums", + description = "test_elicitation_sep1330_enums", + ) { + val schema = ElicitRequestParams.RequestedSchema( + properties = buildJsonObject { + // Untitled single-select + put( + "untitledSingle", + buildJsonObject { + put("type", JsonPrimitive("string")) + put( + "enum", + JsonArray( + listOf(JsonPrimitive("option1"), JsonPrimitive("option2"), JsonPrimitive("option3")), + ), + ) + }, + ) + // Titled single-select + put( + "titledSingle", + buildJsonObject { + put("type", JsonPrimitive("string")) + put( + "oneOf", + JsonArray( + listOf( + buildJsonObject { + put("const", JsonPrimitive("value1")) + put("title", JsonPrimitive("First Option")) + }, + buildJsonObject { + put("const", JsonPrimitive("value2")) + put("title", JsonPrimitive("Second Option")) + }, + buildJsonObject { + put("const", JsonPrimitive("value3")) + put("title", JsonPrimitive("Third Option")) + }, + ), + ), + ) + }, + ) + // Legacy titled (deprecated) + put( + "legacyEnum", + buildJsonObject { + put("type", JsonPrimitive("string")) + put( + "oneOf", + JsonArray( + listOf( + buildJsonObject { + put("const", JsonPrimitive("opt1")) + put("title", JsonPrimitive("Option One")) + }, + buildJsonObject { + put("const", JsonPrimitive("opt2")) + put("title", JsonPrimitive("Option Two")) + }, + buildJsonObject { + put("const", JsonPrimitive("opt3")) + put("title", JsonPrimitive("Option Three")) + }, + ), + ), + ) + }, + ) + // Untitled multi-select + put( + "untitledMulti", + buildJsonObject { + put("type", JsonPrimitive("array")) + put( + "items", + buildJsonObject { + put("type", JsonPrimitive("string")) + put( + "enum", + JsonArray( + listOf( + JsonPrimitive("option1"), + JsonPrimitive("option2"), + JsonPrimitive("option3"), + ), + ), + ) + }, + ) + }, + ) + // Titled multi-select + put( + "titledMulti", + buildJsonObject { + put("type", JsonPrimitive("array")) + put( + "items", + buildJsonObject { + put("type", JsonPrimitive("string")) + put( + "oneOf", + JsonArray( + listOf( + buildJsonObject { + put("const", JsonPrimitive("value1")) + put("title", JsonPrimitive("First Choice")) + }, + buildJsonObject { + put("const", JsonPrimitive("value2")) + put("title", JsonPrimitive("Second Choice")) + }, + buildJsonObject { + put("const", JsonPrimitive("value3")) + put("title", JsonPrimitive("Third Choice")) + }, + ), + ), + ) + }, + ) + }, + ) + }, + ) + val result = createElicitation( + "Please review and update the form fields with defaults", + schema, + ) + CallToolResult(listOf(TextContent(result.content.toString()))) + } + + // 12. Dynamic tool + val server = this + addTool( + name = "test_dynamic_tool", + description = "test_dynamic_tool", + ) { + // Add a temporary tool, triggering listChanged + server.addTool( + name = "test_dynamic_tool_temp", + description = "Temporary dynamic tool", + ) { + CallToolResult(listOf(TextContent("Temporary tool response"))) + } + delay(100.milliseconds) + // Remove the temporary tool, triggering listChanged again + server.removeTool("test_dynamic_tool_temp") + CallToolResult(listOf(TextContent("Dynamic tool executed successfully"))) + } + + // 13. Logging tool + addTool( + name = "test_tool_with_logging", + description = "test_tool_with_logging", + ) { + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive("Tool execution started"), + logger = "conformance", + ), + ), + ) + delay(50.milliseconds) + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive("Tool processing data"), + logger = "conformance", + ), + ), + ) + delay(50.milliseconds) + sendLoggingMessage( + LoggingMessageNotification( + LoggingMessageNotificationParams( + level = LoggingLevel.Info, + data = JsonPrimitive("Tool execution completed"), + logger = "conformance", + ), + ), + ) + CallToolResult(listOf(TextContent("Simple text content"))) + } + + // 14. add_numbers — used by tools_call client scenario + addTool( + name = "add_numbers", + description = "Adds two numbers together", + inputSchema = ToolSchema( + properties = buildJsonObject { + put("a", buildJsonObject { put("type", JsonPrimitive("number")) }) + put("b", buildJsonObject { put("type", JsonPrimitive("number")) }) + }, + required = listOf("a", "b"), + ), + ) { request -> + val a = request.arguments?.get("a")?.jsonPrimitive?.double ?: 0.0 + val b = request.arguments?.get("b")?.jsonPrimitive?.double ?: 0.0 + val sum = a + b + CallToolResult(listOf(TextContent("The sum of $a and $b is $sum"))) + } + + // 15. test_reconnection — SEP-1699, closes SSE stream to test client reconnection + addTool( + name = "test_reconnection", + description = "Tests SSE stream disconnection and client reconnection (SEP-1699)", + ) { + // SDK limitation: cannot access the JSONRPC request ID from the tool handler + // to close the SSE stream. Return success text; this test may fail at the + // conformance runner level because the stream isn't actually closed. + delay(100.milliseconds) + CallToolResult( + listOf( + TextContent( + "Reconnection test completed successfully. " + + "If you received this, the client properly reconnected after stream closure.", + ), + ), + ) + } + + // 16. json_schema_2020_12_tool — SEP-1613 + addTool( + name = "json_schema_2020_12_tool", + description = "Tool with JSON Schema 2020-12 features for conformance testing (SEP-1613)", + inputSchema = ToolSchema( + properties = buildJsonObject { + put( + "name", + buildJsonObject { + put("type", JsonPrimitive("string")) + }, + ) + put( + "address", + buildJsonObject { + put("type", JsonPrimitive("object")) + put( + "properties", + buildJsonObject { + put("street", buildJsonObject { put("type", JsonPrimitive("string")) }) + put("city", buildJsonObject { put("type", JsonPrimitive("string")) }) + }, + ) + }, + ) + }, + ), + ) { request -> + CallToolResult( + listOf(TextContent("JSON Schema 2020-12 tool called with: ${request.arguments}")), + ) + } + + // 17. test_client_elicitation_defaults — used by elicitation-sep1034-client-defaults scenario + addTool( + name = "test_client_elicitation_defaults", + description = "test_client_elicitation_defaults", + ) { + val schema = ElicitRequestParams.RequestedSchema( + properties = buildJsonObject { + put( + "name", + buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("User name")) + put("default", JsonPrimitive("John Doe")) + }, + ) + put( + "age", + buildJsonObject { + put("type", JsonPrimitive("integer")) + put("description", JsonPrimitive("User age")) + put("default", JsonPrimitive(30)) + }, + ) + put( + "score", + buildJsonObject { + put("type", JsonPrimitive("number")) + put("description", JsonPrimitive("User score")) + put("default", JsonPrimitive(95.5)) + }, + ) + put( + "status", + buildJsonObject { + put("type", JsonPrimitive("string")) + put("description", JsonPrimitive("User status")) + put("default", JsonPrimitive("active")) + put( + "enum", + JsonArray( + listOf(JsonPrimitive("active"), JsonPrimitive("inactive"), JsonPrimitive("pending")), + ), + ) + }, + ) + put( + "verified", + buildJsonObject { + put("type", JsonPrimitive("boolean")) + put("description", JsonPrimitive("Verification status")) + put("default", JsonPrimitive(true)) + }, + ) + }, + required = emptyList(), + ) + val result = createElicitation( + "Please review and update the form fields with defaults", + schema, + ) + CallToolResult(listOf(TextContent("Elicitation completed: action=${result.action}, content=${result.content}"))) + } + + // 18. test-tool — simple tool used by auth scenarios + addTool( + name = "test-tool", + description = "Simple test tool for auth scenarios", + ) { + CallToolResult(listOf(TextContent("Test tool executed successfully"))) + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/InMemoryEventStore.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/InMemoryEventStore.kt new file mode 100644 index 000000000..ca7abb3e8 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/InMemoryEventStore.kt @@ -0,0 +1,46 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance + +import io.modelcontextprotocol.kotlin.sdk.server.EventStore +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import java.util.concurrent.ConcurrentHashMap +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@OptIn(ExperimentalUuidApi::class) +class InMemoryEventStore : EventStore { + + private val events = ConcurrentHashMap>() + private val streamEvents = ConcurrentHashMap>() + + override suspend fun storeEvent(streamId: String, message: JSONRPCMessage): String { + val eventId = "$streamId::${System.currentTimeMillis()}_${Uuid.random()}" + events[eventId] = message to streamId + streamEvents.getOrPut(streamId) { mutableListOf() }.add(eventId) + return eventId + } + + override suspend fun replayEventsAfter( + lastEventId: String, + sender: suspend (eventId: String, message: JSONRPCMessage) -> Unit, + ): String { + val streamId = getStreamIdForEventId(lastEventId) + ?: error("Unknown event ID: $lastEventId") + val eventIds = streamEvents[streamId] ?: return streamId + + var found = false + for (eventId in eventIds) { + if (!found) { + if (eventId == lastEventId) found = true + continue + } + val (message, _) = events[eventId] ?: continue + sender(eventId, message) + } + return streamId + } + + override suspend fun getStreamIdForEventId(eventId: String): String? { + val idx = eventId.indexOf("::") + return if (idx >= 0) eventId.substring(0, idx) else null + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/JWTScenario.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/JWTScenario.kt new file mode 100644 index 000000000..9a20681c3 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/JWTScenario.kt @@ -0,0 +1,189 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.forms.submitForm +import io.ktor.http.Parameters +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import java.security.KeyFactory +import java.security.Signature +import java.security.spec.PKCS8EncodedKeySpec +import java.util.Base64 +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +// Client Credentials JWT scenario +internal suspend fun runClientCredentialsJwt(serverUrl: String) { + val ctx = conformanceContext() + val clientId = ctx.requiredString("client_id") + val privateKeyPem = ctx.requiredString("private_key_pem") + val signingAlgorithm = ctx["signing_algorithm"]?.jsonPrimitive?.content ?: "ES256" + + val httpClient = HttpClient(CIO) { + install(SSE) + followRedirects = false + } + + httpClient.use { client -> + val resourceMetadata = discoverResourceMetadata(client, serverUrl) + val authServer = resourceMetadata["authorization_servers"]?.jsonArray?.firstOrNull()?.jsonPrimitive?.content + ?: error("No authorization_servers in resource metadata") + val oauthMetadata = fetchOAuthMetadata(client, authServer) + val tokenEndpoint = oauthMetadata["token_endpoint"]?.jsonPrimitive?.content + ?: error("No token_endpoint in AS metadata") + val issuer = oauthMetadata["issuer"]?.jsonPrimitive?.content + ?: error("No issuer in AS metadata") + + // Create JWT client assertion + val assertion = createJwtAssertion(clientId, issuer, privateKeyPem, signingAlgorithm) + + // Exchange for token + val tokenResponse = client.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "client_credentials") + append("client_id", clientId) + append("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") + append("client_assertion", assertion) + }, + ) + val accessToken = extractAccessToken(tokenResponse) + + withBearerToken(accessToken) { authedClient -> + val transport = StreamableHttpClientTransport(authedClient, serverUrl) + val client = Client( + clientInfo = Implementation("conformance-client-credentials-jwt", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + client.connect(transport) + client.listTools() + client.close() + } + } +} + +// JWT Assertion +@OptIn(ExperimentalUuidApi::class) +private fun createJwtAssertion( + clientId: String, + audience: String, + privateKeyPem: String, + algorithm: String, +): String { + val header = buildJsonObject { + put("alg", algorithm) + put("typ", "JWT") + }.toString() + + val now = System.currentTimeMillis() / 1000 + val payload = buildJsonObject { + put("iss", clientId) + put("sub", clientId) + put("aud", audience) + put("iat", now) + put("exp", now + 300) + put("jti", Uuid.random().toString()) + }.toString() + + val headerB64 = Base64.getUrlEncoder().withoutPadding().encodeToString(header.toByteArray()) + val payloadB64 = Base64.getUrlEncoder().withoutPadding().encodeToString(payload.toByteArray()) + val signingInput = "$headerB64.$payloadB64" + + val signature = signJwt(signingInput, privateKeyPem, algorithm) + return "$signingInput.$signature" +} + +private fun signJwt(input: String, privateKeyPem: String, algorithm: String): String { + val pemBody = privateKeyPem + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replace("-----BEGIN EC PRIVATE KEY-----", "") + .replace("-----END EC PRIVATE KEY-----", "") + .replace("-----BEGIN RSA PRIVATE KEY-----", "") + .replace("-----END RSA PRIVATE KEY-----", "") + .replace("\n", "") + .replace("\r", "") + .trim() + + val keyBytes = Base64.getDecoder().decode(pemBody) + val keySpec = PKCS8EncodedKeySpec(keyBytes) + + val (keyAlgorithm, signatureAlgorithm) = when (algorithm) { + "ES256" -> "EC" to "SHA256withECDSA" + "RS256" -> "RSA" to "SHA256withRSA" + else -> error("Unsupported signing algorithm: $algorithm") + } + + val keyFactory = KeyFactory.getInstance(keyAlgorithm) + val privateKey = keyFactory.generatePrivate(keySpec) + + val sig = Signature.getInstance(signatureAlgorithm) + sig.initSign(privateKey) + sig.update(input.toByteArray()) + val rawSignature = sig.sign() + + // For EC, convert DER to raw r||s format for JWS + val signatureBytes = if (keyAlgorithm == "EC") { + derToRawEcSignature(rawSignature) + } else { + rawSignature + } + + return Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes) +} + +private fun derToRawEcSignature(der: ByteArray): ByteArray { + // DER format: 0x30 len 0x02 rLen r 0x02 sLen s + require(der.size >= 2) { "DER signature too short" } + + var offset = 2 // skip SEQUENCE tag and length + if (der[1].toInt() and 0x80 != 0) { + offset += (der[1].toInt() and 0x7f) + } + + // Read r + require(offset < der.size) { "DER signature truncated before r tag" } + check(der[offset] == 0x02.toByte()) { "Expected INTEGER tag for r" } + offset++ + require(offset < der.size) { "DER signature truncated before r length" } + val rLen = der[offset].toInt() and 0xff + offset++ + require(offset + rLen <= der.size) { "DER signature truncated in r value" } + val r = der.copyOfRange(offset, offset + rLen) + offset += rLen + + // Read s + require(offset < der.size) { "DER signature truncated before s tag" } + check(der[offset] == 0x02.toByte()) { "Expected INTEGER tag for s" } + offset++ + require(offset < der.size) { "DER signature truncated before s length" } + val sLen = der[offset].toInt() and 0xff + offset++ + require(offset + sLen <= der.size) { "DER signature truncated in s value" } + val s = der.copyOfRange(offset, offset + sLen) + + // Each component should be 32 bytes for P-256 + val componentLen = 32 + val result = ByteArray(componentLen * 2) + + // Copy r (may need padding or trimming of leading zero) + val rStart = if (r.size > componentLen) r.size - componentLen else 0 + val rDest = if (r.size < componentLen) componentLen - r.size else 0 + r.copyInto(result, rDest, rStart, r.size) + + // Copy s + val sStart = if (s.size > componentLen) s.size - componentLen else 0 + val sDest = componentLen + if (s.size < componentLen) componentLen - s.size else 0 + s.copyInto(result, sDest, sStart, s.size) + + return result +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/authCodeFlow.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/authCodeFlow.kt new file mode 100644 index 000000000..c59e8e003 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/authCodeFlow.kt @@ -0,0 +1,154 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpSend +import io.ktor.client.plugins.plugin +import io.ktor.client.plugins.sse.SSE +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import java.util.UUID + +internal suspend fun runAuthClient(serverUrl: String) { + val httpClient = HttpClient(CIO) { + install(SSE) + followRedirects = false + } + + var accessToken: String? = null + var authAttempts = 0 + // Cache discovery and credentials across retries + var cachedDiscovery: DiscoveryResult? = null + var cachedCredentials: ClientCredentials? = null + + httpClient.plugin(HttpSend).intercept { request -> + // Add existing token if available + if (accessToken != null) { + request.headers.remove(HttpHeaders.Authorization) + request.headers.append(HttpHeaders.Authorization, "Bearer $accessToken") + } + + val response = execute(request) + val status = response.response.status + + // Determine if we need to (re-)authorize + val needsAuth = status == HttpStatusCode.Unauthorized + val wwwAuth = response.response.headers[HttpHeaders.WWWAuthenticate] ?: "" + val stepUpScope = if (status == HttpStatusCode.Forbidden) parseStepUpScope(wwwAuth) else null + val needsStepUp = stepUpScope != null + + if ((needsAuth || needsStepUp) && authAttempts < 3) { + authAttempts++ + + // Discover metadata (cache across retries) + if (cachedDiscovery == null) { + val resourceMetadataUrl = extractParam(wwwAuth, "resource_metadata") + cachedDiscovery = discoverOAuthMetadata(httpClient, serverUrl, resourceMetadataUrl) + } + val discovery: DiscoveryResult = cachedDiscovery + + // Validate PRM resource matches server URL (RFC 8707) + val discoveredResource = discovery.resourceUrl + if (discoveredResource != null) { + val normalizedResource = discoveredResource.trimEnd('/') + val normalizedServerUrl = serverUrl.trimEnd('/') + val matches = normalizedServerUrl == normalizedResource || + normalizedServerUrl.startsWith("$normalizedResource/") + require(matches) { + "PRM resource mismatch: resource='$discoveredResource' does not match server URL='$serverUrl'" + } + } + + val metadata = discovery.asMetadata + + val authEndpoint = metadata["authorization_endpoint"]?.jsonPrimitive?.content + ?: error("No authorization_endpoint in metadata") + val tokenEndpoint = metadata["token_endpoint"]?.jsonPrimitive?.content + ?: error("No token_endpoint in metadata") + + val tokenEndpointAuthMethods = metadata["token_endpoint_auth_methods_supported"] + ?.jsonArray?.map { it.jsonPrimitive.content } + ?: listOf("client_secret_post") + val tokenAuthMethod = tokenEndpointAuthMethods.firstOrNull() ?: "client_secret_post" + + // Verify PKCE support + verifyPkceSupport(metadata) + + // Resolve client credentials (cache across retries) + if (cachedCredentials == null) { + cachedCredentials = resolveClientCredentials(httpClient, metadata) + } + val creds: ClientCredentials = cachedCredentials + + // Determine scope + val scope = if (needsStepUp) { + stepUpScope + } else { + val wwwAuthScope = extractParam(wwwAuth, "scope") + selectScope(wwwAuthScope, discovery.scopesSupported) + } + + // PKCE + val codeVerifier = generateCodeVerifier() + val codeChallenge = generateCodeChallenge(codeVerifier) + + // CSRF state parameter + val state = UUID.randomUUID().toString() + + // Build authorization URL + val authUrl = buildAuthorizationUrl( + authEndpoint, + creds.clientId, + CALLBACK_URL, + codeChallenge, + scope, + discovery.resourceUrl, + state, + ) + + // Follow the authorization redirect to get auth code + val authCode = followAuthorizationRedirect(httpClient, authUrl, CALLBACK_URL, state) + + // Exchange code for tokens + accessToken = exchangeCodeForTokens( + httpClient, + tokenEndpoint, + authCode, + creds.clientId, + creds.clientSecret, + CALLBACK_URL, + codeVerifier, + tokenAuthMethod, + discovery.resourceUrl, + ) + + // Retry the original request with the token + request.headers.remove(HttpHeaders.Authorization) + request.headers.append(HttpHeaders.Authorization, "Bearer $accessToken") + execute(request) + } else { + response + } + } + + httpClient.use { client -> + val transport = StreamableHttpClientTransport(client, serverUrl) + val mcpClient = Client( + clientInfo = Implementation("test-auth-client", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + mcpClient.connect(transport) + mcpClient.listTools() + mcpClient.callTool(CallToolRequest(CallToolRequestParams(name = "test-tool"))) + mcpClient.close() + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/basicScenario.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/basicScenario.kt new file mode 100644 index 000000000..a0d6d864d --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/basicScenario.kt @@ -0,0 +1,54 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.header +import io.ktor.http.HttpHeaders +import io.ktor.http.Parameters +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import java.util.Base64 + +// Client Credentials Basic scenario +internal suspend fun runClientCredentialsBasic(serverUrl: String) { + val ctx = conformanceContext() + val clientId = ctx.requiredString("client_id") + val clientSecret = ctx.requiredString("client_secret") + + val httpClient = HttpClient(CIO) { + install(SSE) + followRedirects = false + } + + httpClient.use { client -> + val tokenEndpoint = discoverTokenEndpoint(client, serverUrl) + + // Exchange credentials for token using Basic auth + val basicAuth = Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray()) + val tokenResponse = client.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "client_credentials") + }, + ) { + header(HttpHeaders.Authorization, "Basic $basicAuth") + } + val accessToken = extractAccessToken(tokenResponse) + + withBearerToken(accessToken) { authedClient -> + val transport = StreamableHttpClientTransport(authedClient, serverUrl) + val client = Client( + clientInfo = Implementation("conformance-client-credentials-basic", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + client.connect(transport) + client.listTools() + client.close() + } + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/clientRegistration.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/clientRegistration.kt new file mode 100644 index 000000000..8e84a1587 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/clientRegistration.kt @@ -0,0 +1,73 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.contentType +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put + +internal data class ClientCredentials(val clientId: String, val clientSecret: String?) + +/** + * Resolve client credentials per spec priority: + * 1. Pre-registered (from MCP_CONFORMANCE_CONTEXT) + * 2. CIMD (client_id_metadata_document_supported) + * 3. Dynamic registration (registration_endpoint) + * 4. Error + */ +internal suspend fun resolveClientCredentials(httpClient: HttpClient, asMetadata: JsonObject): ClientCredentials { + // 1. Pre-registered + val contextJson = System.getenv("MCP_CONFORMANCE_CONTEXT") + if (contextJson != null) { + val ctx = json.parseToJsonElement(contextJson).jsonObject + val clientId = ctx["client_id"]?.jsonPrimitive?.content + if (clientId != null) { + val clientSecret = ctx["client_secret"]?.jsonPrimitive?.content + return ClientCredentials(clientId, clientSecret) + } + } + + // 2. CIMD + val cimdSupported = asMetadata["client_id_metadata_document_supported"] + ?.jsonPrimitive?.content?.toBoolean() ?: false + if (cimdSupported) { + return ClientCredentials(CIMD_CLIENT_METADATA_URL, null) + } + + // 3. Dynamic registration + val registrationEndpoint = asMetadata["registration_endpoint"]?.jsonPrimitive?.content + if (registrationEndpoint != null) { + return dynamicClientRegistration(httpClient, registrationEndpoint) + } + + error("No way to register client: no pre-registered credentials, CIMD not supported, and no registration_endpoint") +} + +private suspend fun dynamicClientRegistration( + httpClient: HttpClient, + registrationEndpoint: String, +): ClientCredentials { + val regBody = buildJsonObject { + put("client_name", "test-auth-client") + put("redirect_uris", buildJsonArray { add(kotlinx.serialization.json.JsonPrimitive(CALLBACK_URL)) }) + put("grant_types", buildJsonArray { add(kotlinx.serialization.json.JsonPrimitive("authorization_code")) }) + put("response_types", buildJsonArray { add(kotlinx.serialization.json.JsonPrimitive("code")) }) + put("token_endpoint_auth_method", "client_secret_post") + } + + val response = httpClient.post(registrationEndpoint) { + contentType(ContentType.Application.Json) + setBody(regBody.toString()) + } + val regJson = json.parseToJsonElement(response.bodyAsText()).jsonObject + val clientId = regJson["client_id"]?.jsonPrimitive?.content ?: error("No client_id in registration response") + val clientSecret = regJson["client_secret"]?.jsonPrimitive?.content + return ClientCredentials(clientId, clientSecret) +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/crossAppAccessScenario.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/crossAppAccessScenario.kt new file mode 100644 index 000000000..db4b7f0f6 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/crossAppAccessScenario.kt @@ -0,0 +1,82 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.header +import io.ktor.http.HttpHeaders +import io.ktor.http.Parameters +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import java.util.Base64 + +// SEP-990 Enterprise Managed OAuth: Cross-App Access complete flow +internal suspend fun runCrossAppAccess(serverUrl: String) { + val ctx = conformanceContext() + val clientId = ctx.requiredString("client_id") + val clientSecret = ctx.requiredString("client_secret") + val idpIdToken = ctx.requiredString("idp_id_token") + val idpTokenEndpoint = ctx.requiredString("idp_token_endpoint") + + val httpClient = HttpClient(CIO) { + install(SSE) + followRedirects = false + } + + httpClient.use { client -> + // Discover PRM + AS metadata + val resourceMeta = discoverResourceMetadata(client, serverUrl) + val resourceUrl = resourceMeta["resource"]?.jsonPrimitive?.content + ?: error("No resource in resource metadata") + val authServer = resourceMeta["authorization_servers"]?.jsonArray?.firstOrNull()?.jsonPrimitive?.content + ?: error("No authorization_servers in resource metadata") + val asMeta = fetchOAuthMetadata(client, authServer) + val tokenEndpoint = asMeta["token_endpoint"]?.jsonPrimitive?.content + ?: error("No token_endpoint in AS metadata") + + // RFC 8693 Token Exchange at IDP: exchange ID token for ID-JAG + val idpResponse = client.submitForm( + url = idpTokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") + append("subject_token", idpIdToken) + append("subject_token_type", "urn:ietf:params:oauth:token-type:id_token") + append("requested_token_type", "urn:ietf:params:oauth:token-type:id-jag") + append("audience", authServer) + append("resource", resourceUrl) + }, + ) + val idJag = extractAccessToken(idpResponse) + + // RFC 7523 JWT Bearer Grant at AS with Basic auth + val basicAuth = Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray()) + val asResponse = client.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer") + append("assertion", idJag) + }, + ) { + header(HttpHeaders.Authorization, "Basic $basicAuth") + } + val accessToken = extractAccessToken(asResponse) + + // Use access token for MCP requests + withBearerToken(accessToken) { authedClient -> + val transport = StreamableHttpClientTransport(authedClient, serverUrl) + val mcpClient = Client( + clientInfo = Implementation("conformance-cross-app-access", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + mcpClient.connect(transport) + mcpClient.listTools() + mcpClient.close() + } + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/discovery.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/discovery.kt new file mode 100644 index 000000000..73979db5f --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/discovery.kt @@ -0,0 +1,48 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.isSuccess +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +internal data class DiscoveryResult( + val asMetadata: JsonObject, + val resourceUrl: String?, + val scopesSupported: List?, +) + +internal suspend fun discoverOAuthMetadata( + httpClient: HttpClient, + serverUrl: String, + resourceMetadataUrl: String?, +): DiscoveryResult { + // Get resource metadata + val resourceMeta = if (resourceMetadataUrl != null) { + val resp = httpClient.get(resourceMetadataUrl) + if (!resp.status.isSuccess()) { + error("Failed to fetch resource metadata from $resourceMetadataUrl: ${resp.status}") + } + json.parseToJsonElement(resp.bodyAsText()).jsonObject + } else { + discoverResourceMetadata(httpClient, serverUrl) + } + + val resourceUrl = resourceMeta["resource"]?.jsonPrimitive?.content + val scopesSupported = resourceMeta["scopes_supported"] + ?.jsonArray?.map { it.jsonPrimitive.content } + val authServer = resourceMeta["authorization_servers"]?.jsonArray?.firstOrNull()?.jsonPrimitive?.content + + val oauthMeta = if (authServer != null) { + fetchOAuthMetadata(httpClient, authServer) + } else { + // Fallback: try well-known on server URL origin + val origin = extractOrigin(serverUrl) + fetchOAuthMetadata(httpClient, origin) + } + + return DiscoveryResult(oauthMeta, resourceUrl, scopesSupported) +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/pkce.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/pkce.kt new file mode 100644 index 000000000..e1c5419ed --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/pkce.kt @@ -0,0 +1,30 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import java.security.MessageDigest +import java.util.Base64 + +internal fun generateCodeVerifier(): String { + val bytes = ByteArray(32) + java.security.SecureRandom().nextBytes(bytes) + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes) +} + +internal fun generateCodeChallenge(verifier: String): String { + val digest = MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray(Charsets.US_ASCII)) + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest) +} + +/** + * Verify that the AS metadata advertises S256 in code_challenge_methods_supported. + * Abort if PKCE S256 is not supported. + */ +internal fun verifyPkceSupport(asMetadata: JsonObject) { + val methods = asMetadata["code_challenge_methods_supported"] + ?.jsonArray?.map { it.jsonPrimitive.content } + require(methods != null && "S256" in methods) { + "Authorization server does not support PKCE S256 (code_challenge_methods_supported: $methods)" + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt new file mode 100644 index 000000000..4964f20d2 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt @@ -0,0 +1,32 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.modelcontextprotocol.kotlin.sdk.conformance.scenarioHandlers + +// Registration +fun registerAuthScenarios() { + val authScenarios = listOf( + "auth/metadata-default", + "auth/metadata-var1", + "auth/metadata-var2", + "auth/metadata-var3", + "auth/basic-cimd", + "auth/scope-from-www-authenticate", + "auth/scope-from-scopes-supported", + "auth/scope-omitted-when-undefined", + "auth/scope-step-up", + "auth/scope-retry-limit", + "auth/token-endpoint-auth-basic", + "auth/token-endpoint-auth-post", + "auth/token-endpoint-auth-none", + "auth/resource-mismatch", + "auth/pre-registration", + "auth/2025-03-26-oauth-metadata-backcompat", + "auth/2025-03-26-oauth-endpoint-fallback", + ) + for (name in authScenarios) { + scenarioHandlers[name] = ::runAuthClient + } + scenarioHandlers["auth/client-credentials-jwt"] = ::runClientCredentialsJwt + scenarioHandlers["auth/client-credentials-basic"] = ::runClientCredentialsBasic + scenarioHandlers["auth/cross-app-access-complete-flow"] = ::runCrossAppAccess +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/scopeHandling.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/scopeHandling.kt new file mode 100644 index 000000000..33b69ceb0 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/scopeHandling.kt @@ -0,0 +1,29 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +internal fun extractParam(wwwAuth: String, param: String): String? { + val regex = Regex("""$param="([^"]+)"""") + return regex.find(wwwAuth)?.groupValues?.get(1) +} + +/** + * Select scope per MCP spec priority: + * 1. scope from WWW-Authenticate header + * 2. scopes_supported from Protected Resource Metadata (space-joined) + * 3. null (omit scope entirely) + */ +internal fun selectScope(wwwAuthScope: String?, scopesSupported: List?): String? { + if (wwwAuthScope != null) return wwwAuthScope + if (!scopesSupported.isNullOrEmpty()) return scopesSupported.joinToString(" ") + return null +} + +/** + * Detect 403 with error="insufficient_scope" and extract the new scope. + * Returns the scope string if step-up is needed, null otherwise. + */ +internal fun parseStepUpScope(wwwAuth: String?): String? { + if (wwwAuth == null) return null + val error = extractParam(wwwAuth, "error") + if (error != "insufficient_scope") return null + return extractParam(wwwAuth, "scope") +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/tokenExchange.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/tokenExchange.kt new file mode 100644 index 000000000..0c79bb97d --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/tokenExchange.kt @@ -0,0 +1,174 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.Parameters +import io.ktor.http.isSuccess +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.net.URI +import java.net.URLEncoder +import java.util.Base64 + +internal fun buildAuthorizationUrl( + authEndpoint: String, + clientId: String, + redirectUri: String, + codeChallenge: String, + scope: String?, + resource: String?, + state: String, +): String { + val params = buildString { + append("response_type=code") + append("&client_id=${URLEncoder.encode(clientId, "UTF-8")}") + append("&redirect_uri=${URLEncoder.encode(redirectUri, "UTF-8")}") + append("&code_challenge=${URLEncoder.encode(codeChallenge, "UTF-8")}") + append("&code_challenge_method=S256") + append("&state=${URLEncoder.encode(state, "UTF-8")}") + if (scope != null) { + append("&scope=${URLEncoder.encode(scope, "UTF-8")}") + } + if (resource != null) { + append("&resource=${URLEncoder.encode(resource, "UTF-8")}") + } + } + return if (authEndpoint.contains("?")) "$authEndpoint&$params" else "$authEndpoint?$params" +} + +internal suspend fun followAuthorizationRedirect( + httpClient: HttpClient, + authUrl: String, + expectedCallbackUrl: String, + expectedState: String, +): String { + val response = httpClient.get(authUrl) + + if (response.status == HttpStatusCode.Found || + response.status == HttpStatusCode.MovedPermanently || + response.status == HttpStatusCode.TemporaryRedirect || + response.status == HttpStatusCode.SeeOther + ) { + val location = response.headers[HttpHeaders.Location] + ?: error("No Location header in redirect response") + + require(location.startsWith(expectedCallbackUrl)) { + "Redirect location does not match expected callback URL" + } + + val uri = URI(location) + val queryParams = uri.query?.split("&")?.mapNotNull { + val parts = it.split("=", limit = 2) + if (parts.size == 2) parts[0] to java.net.URLDecoder.decode(parts[1], "UTF-8") else null + }?.toMap() ?: emptyMap() + + val returnedState = queryParams["state"] + require(returnedState == expectedState) { + "State parameter mismatch in authorization redirect" + } + + return queryParams["code"] ?: error("No authorization code in redirect response") + } + + error("Expected redirect from auth endpoint, got ${response.status}") +} + +internal suspend fun exchangeCodeForTokens( + httpClient: HttpClient, + tokenEndpoint: String, + code: String, + clientId: String, + clientSecret: String?, + redirectUri: String, + codeVerifier: String, + tokenAuthMethod: String, + resource: String?, +): String { + val response = when (tokenAuthMethod) { + "client_secret_basic" -> { + val basicAuth = Base64.getEncoder() + .encodeToString("$clientId:${clientSecret ?: ""}".toByteArray()) + httpClient.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "authorization_code") + append("code", code) + append("redirect_uri", redirectUri) + append("code_verifier", codeVerifier) + if (resource != null) { + append("resource", resource) + } + }, + ) { + header(HttpHeaders.Authorization, "Basic $basicAuth") + } + } + + "none" -> { + httpClient.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "authorization_code") + append("code", code) + append("client_id", clientId) + append("redirect_uri", redirectUri) + append("code_verifier", codeVerifier) + if (resource != null) { + append("resource", resource) + } + }, + ) + } + + else -> { + // client_secret_post (default) + httpClient.submitForm( + url = tokenEndpoint, + formParameters = Parameters.build { + append("grant_type", "authorization_code") + append("code", code) + append("client_id", clientId) + if (clientSecret != null) { + append("client_secret", clientSecret) + } + append("redirect_uri", redirectUri) + append("code_verifier", codeVerifier) + if (resource != null) { + append("resource", resource) + } + }, + ) + } + } + + // Check HTTP status + if (!response.status.isSuccess()) { + val body = response.bodyAsText() + val errorDetail = try { + val obj = json.parseToJsonElement(body).jsonObject + val err = obj["error"]?.jsonPrimitive?.content ?: "unknown" + val desc = obj["error_description"]?.jsonPrimitive?.content + if (desc != null) "$err: $desc" else err + } catch (_: Exception) { + body + } + error("Token exchange failed (${response.status}): $errorDetail") + } + + val tokenJson = json.parseToJsonElement(response.bodyAsText()).jsonObject + + // Check for error field in response body (some servers return 200 with error) + val errorField = tokenJson["error"]?.jsonPrimitive?.content + if (errorField != null) { + val desc = tokenJson["error_description"]?.jsonPrimitive?.content + error("Token exchange error: $errorField${if (desc != null) " - $desc" else ""}") + } + + return tokenJson["access_token"]?.jsonPrimitive?.content + ?: error("No access_token in token response") +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/utils.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/utils.kt new file mode 100644 index 000000000..4bc75a0e3 --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/utils.kt @@ -0,0 +1,120 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpSend +import io.ktor.client.plugins.plugin +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpHeaders +import io.ktor.http.isSuccess +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.net.URI +import kotlin.text.ifEmpty + +internal val json = Json { ignoreUnknownKeys = true } + +internal fun conformanceContext(): JsonObject { + val contextJson = System.getenv("MCP_CONFORMANCE_CONTEXT") + ?: error("MCP_CONFORMANCE_CONTEXT not set") + return json.parseToJsonElement(contextJson).jsonObject +} + +internal fun JsonObject.requiredString(key: String): String = this[key]?.jsonPrimitive?.content ?: error("Missing $key") + +internal const val CIMD_CLIENT_METADATA_URL = "https://conformance-test.local/client-metadata.json" +internal const val CALLBACK_URL = "http://localhost:3000/callback" + +internal fun extractOrigin(url: String): String { + val uri = URI(url) + return "${uri.scheme}://${uri.host}${if (uri.port > 0) ":${uri.port}" else ""}" +} + +internal suspend fun discoverResourceMetadata(httpClient: HttpClient, serverUrl: String): JsonObject { + val origin = extractOrigin(serverUrl) + val path = URI(serverUrl).path.ifEmpty { "/" } + + // Try RFC 9728 format first: /.well-known/oauth-protected-resource/ + val wellKnownUrl = "$origin/.well-known/oauth-protected-resource$path" + val response = httpClient.get(wellKnownUrl) + if (response.status.isSuccess()) { + return json.parseToJsonElement(response.bodyAsText()).jsonObject + } + + // Fallback: try root + val fallbackUrl = "$origin/.well-known/oauth-protected-resource" + val fallbackResponse = httpClient.get(fallbackUrl) + if (!fallbackResponse.status.isSuccess()) { + error( + "Failed to discover resource metadata at $wellKnownUrl (${response.status}) and $fallbackUrl (${fallbackResponse.status})", + ) + } + return json.parseToJsonElement(fallbackResponse.bodyAsText()).jsonObject +} + +internal suspend fun fetchOAuthMetadata(httpClient: HttpClient, authServerUrl: String): JsonObject { + val origin = extractOrigin(authServerUrl) + val path = URI(authServerUrl).path.ifEmpty { "/" } + + // RFC 8414 §3: /.well-known/oauth-authorization-server/ + val oauthUrl = "$origin/.well-known/oauth-authorization-server$path" + val oauthResponse = httpClient.get(oauthUrl) + if (oauthResponse.status.isSuccess()) { + return json.parseToJsonElement(oauthResponse.bodyAsText()).jsonObject + } + + // OIDC Discovery with path insertion: /.well-known/openid-configuration/ + val oidcPathUrl = "$origin/.well-known/openid-configuration$path" + val oidcPathResponse = httpClient.get(oidcPathUrl) + if (oidcPathResponse.status.isSuccess()) { + return json.parseToJsonElement(oidcPathResponse.bodyAsText()).jsonObject + } + + // Fallback: OpenID Connect discovery (issuer + /.well-known/openid-configuration) + val oidcUrl = "$authServerUrl/.well-known/openid-configuration" + val oidcResponse = httpClient.get(oidcUrl) + if (oidcResponse.status.isSuccess()) { + return json.parseToJsonElement(oidcResponse.bodyAsText()).jsonObject + } + + error( + "Failed to fetch OAuth metadata from $oauthUrl (${oauthResponse.status}) and $oidcUrl (${oidcResponse.status})", + ) +} + +internal suspend fun discoverTokenEndpoint(httpClient: HttpClient, serverUrl: String): String { + val resourceMetadata = discoverResourceMetadata(httpClient, serverUrl) + val authServer = resourceMetadata["authorization_servers"]?.jsonArray?.firstOrNull()?.jsonPrimitive?.content + ?: error("No authorization_servers in resource metadata") + + val oauthMetadata = fetchOAuthMetadata(httpClient, authServer) + return oauthMetadata["token_endpoint"]?.jsonPrimitive?.content + ?: error("No token_endpoint") +} + +internal suspend fun extractAccessToken(tokenResponse: HttpResponse): String { + if (!tokenResponse.status.isSuccess()) { + error("Token request failed: ${tokenResponse.status}") + } + val tokenJson = json.parseToJsonElement(tokenResponse.bodyAsText()).jsonObject + return tokenJson["access_token"]?.jsonPrimitive?.content + ?: error("No access_token in token response") +} + +internal suspend fun withBearerToken(accessToken: String, block: suspend (HttpClient) -> T): T { + val client = HttpClient(CIO) { + install(SSE) + } + client.plugin(HttpSend).intercept { request -> + request.headers.remove(HttpHeaders.Authorization) + request.headers.append(HttpHeaders.Authorization, "Bearer $accessToken") + execute(request) + } + return client.use { block(it) } +} diff --git a/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt deleted file mode 100644 index 09ea50bf9..000000000 --- a/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt +++ /dev/null @@ -1,85 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.conformance - -import io.github.oshai.kotlinlogging.KotlinLogging -import io.ktor.client.HttpClient -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.sse.SSE -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport -import io.modelcontextprotocol.kotlin.sdk.shared.Transport -import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest -import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams -import io.modelcontextprotocol.kotlin.sdk.types.Implementation -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject - -private val logger = KotlinLogging.logger {} - -fun main(args: Array) { - require(args.isNotEmpty()) { - "Server URL must be provided as an argument" - } - - val serverUrl = args.last() - logger.info { "Connecting to test server at: $serverUrl" } - - val httpClient = HttpClient(CIO) { - install(SSE) - } - val transport: Transport = StreamableHttpClientTransport(httpClient, serverUrl) - - val client = Client( - clientInfo = Implementation( - name = "kotlin-conformance-client", - version = "1.0.0", - ), - ) - - var exitCode = 0 - - runBlocking { - try { - client.connect(transport) - logger.info { "✅ Connected to server successfully" } - - try { - val tools = client.listTools() - logger.info { "Available tools: ${tools.tools.map { it.name }}" } - - if (tools.tools.isNotEmpty()) { - val toolName = tools.tools.first().name - logger.info { "Calling tool: $toolName" } - - val result = client.callTool( - CallToolRequest( - params = CallToolRequestParams( - name = toolName, - arguments = buildJsonObject { - put("input", JsonPrimitive("test")) - }, - ), - ), - ) - logger.info { "Tool result: ${result.content}" } - } - } catch (e: Exception) { - logger.debug(e) { "Error during tool operations (may be expected for some scenarios)" } - } - - logger.info { "✅ Client operations completed successfully" } - } catch (e: Exception) { - logger.error(e) { "❌ Client failed" } - exitCode = 1 - } finally { - try { - transport.close() - } catch (e: Exception) { - logger.warn(e) { "Error closing transport" } - } - httpClient.close() - } - } - - kotlin.system.exitProcess(exitCode) -} diff --git a/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt deleted file mode 100644 index b3331ddc9..000000000 --- a/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt +++ /dev/null @@ -1,426 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.conformance - -import io.github.oshai.kotlinlogging.KotlinLogging -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.ApplicationCall -import io.ktor.server.application.install -import io.ktor.server.cio.CIO -import io.ktor.server.engine.embeddedServer -import io.ktor.server.request.header -import io.ktor.server.request.receiveText -import io.ktor.server.response.header -import io.ktor.server.response.respond -import io.ktor.server.response.respondText -import io.ktor.server.response.respondTextWriter -import io.ktor.server.routing.delete -import io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.server.routing.routing -import io.ktor.server.websocket.WebSockets -import io.ktor.server.websocket.webSocket -import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions -import io.modelcontextprotocol.kotlin.sdk.server.WebSocketMcpServerTransport -import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport -import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions -import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult -import io.modelcontextprotocol.kotlin.sdk.types.Implementation -import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError -import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest -import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse -import io.modelcontextprotocol.kotlin.sdk.types.McpJson -import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument -import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage -import io.modelcontextprotocol.kotlin.sdk.types.RPCError -import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult -import io.modelcontextprotocol.kotlin.sdk.types.RequestId -import io.modelcontextprotocol.kotlin.sdk.types.Role -import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.types.TextContent -import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents -import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.put -import java.util.UUID -import java.util.concurrent.ConcurrentHashMap - -private val logger = KotlinLogging.logger {} -private val serverTransports = ConcurrentHashMap() -private val jsonFormat = Json { ignoreUnknownKeys = true } - -private const val SESSION_CREATION_TIMEOUT_MS = 2000L -private const val REQUEST_TIMEOUT_MS = 10_000L -private const val MESSAGE_QUEUE_CAPACITY = 256 - -private fun isInitializeRequest(json: JsonElement): Boolean = - json is JsonObject && json["method"]?.jsonPrimitive?.contentOrNull == "initialize" - -@Suppress("CyclomaticComplexMethod", "LongMethod") -fun main(args: Array) { - val port = args.getOrNull(0)?.toIntOrNull() ?: 3000 - - logger.info { "Starting MCP Conformance Server on port $port" } - - embeddedServer(CIO, port = port, host = "127.0.0.1") { - install(WebSockets) - - routing { - webSocket("/ws") { - logger.info { "WebSocket connection established" } - val transport = WebSocketMcpServerTransport(this) - val server = createConformanceServer() - - try { - server.createSession(transport) - } catch (e: Exception) { - logger.error(e) { "Error in WebSocket session" } - throw e - } - } - - get("/mcp") { - val sessionId = call.request.header("mcp-session-id") - ?: run { - call.respond(HttpStatusCode.BadRequest, "Missing mcp-session-id header") - return@get - } - val transport = serverTransports[sessionId] - ?: run { - call.respond(HttpStatusCode.BadRequest, "Invalid mcp-session-id") - return@get - } - transport.stream(call) - } - - 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(), - buildJsonObject { - put("jsonrpc", "2.0") - put( - "error", - buildJsonObject { - put("code", -32700) - put("message", "Parse error: ${e.message}") - }, - ) - put("id", JsonNull) - }, - ), - ) - return@post - } - - val transport = sessionId?.let { serverTransports[it] } - if (transport != null) { - logger.debug { "Using existing transport for session: $sessionId" } - transport.handleRequest(call, jsonElement) - } else { - if (isInitializeRequest(jsonElement)) { - val newSessionId = UUID.randomUUID().toString() - logger.info { "Creating new session with ID: $newSessionId" } - - val newTransport = HttpServerTransport(newSessionId) - serverTransports[newSessionId] = newTransport - - val mcpServer = createConformanceServer() - call.response.header("mcp-session-id", newSessionId) - - val sessionReady = CompletableDeferred() - CoroutineScope(Dispatchers.IO).launch { - try { - mcpServer.createSession(newTransport) - sessionReady.complete(Unit) - } catch (e: Exception) { - logger.error(e) { "Failed to create session" } - serverTransports.remove(newSessionId) - newTransport.close() - sessionReady.completeExceptionally(e) - } - } - - val sessionCreated = withTimeoutOrNull(SESSION_CREATION_TIMEOUT_MS) { - sessionReady.await() - } - - if (sessionCreated == null) { - logger.error { "Session creation timed out" } - serverTransports.remove(newSessionId) - call.respond( - HttpStatusCode.InternalServerError, - jsonFormat.encodeToString( - JsonObject.serializer(), - buildJsonObject { - put("jsonrpc", "2.0") - put( - "error", - buildJsonObject { - put("code", -32000) - put("message", "Session creation timed out") - }, - ) - put("id", JsonNull) - }, - ), - ) - return@post - } - - newTransport.handleRequest(call, jsonElement) - } else { - logger.warn { "Invalid request: no session ID or not an initialization request" } - call.respond( - HttpStatusCode.BadRequest, - jsonFormat.encodeToString( - JsonObject.serializer(), - buildJsonObject { - put("jsonrpc", "2.0") - put( - "error", - buildJsonObject { - put("code", -32000) - put("message", "Bad Request: No valid session ID provided") - }, - ) - put("id", JsonNull) - }, - ), - ) - } - } - } - - delete("/mcp") { - val sessionId = call.request.header("mcp-session-id") - val transport = sessionId?.let { serverTransports[it] } - if (transport != null) { - logger.info { "Terminating session: $sessionId" } - serverTransports.remove(sessionId) - transport.close() - call.respond(HttpStatusCode.OK) - } else { - logger.warn { "Invalid session termination request: $sessionId" } - call.respond(HttpStatusCode.BadRequest, "Invalid or missing session ID") - } - } - } - }.start(wait = true) -} - -@Suppress("LongMethod") -private fun createConformanceServer(): Server { - val server = Server( - Implementation( - name = "kotlin-conformance-server", - version = "1.0.0", - ), - ServerOptions( - capabilities = ServerCapabilities( - tools = ServerCapabilities.Tools(listChanged = true), - resources = ServerCapabilities.Resources(subscribe = true, listChanged = true), - prompts = ServerCapabilities.Prompts(listChanged = true), - ), - ), - ) - - server.addTool( - name = "test-tool", - description = "A test tool for conformance testing", - inputSchema = ToolSchema( - properties = buildJsonObject { - put( - "input", - buildJsonObject { - put("type", "string") - put("description", "Test input parameter") - }, - ) - }, - required = listOf("input"), - ), - ) { request -> - val input = (request.params.arguments?.get("input") as? JsonPrimitive)?.content ?: "no input" - CallToolResult( - content = listOf(TextContent("Tool executed with input: $input")), - ) - } - - server.addResource( - uri = "test://test-resource", - name = "Test Resource", - description = "A test resource for conformance testing", - mimeType = "text/plain", - ) { request -> - ReadResourceResult( - contents = listOf( - TextResourceContents("Test resource content", request.params.uri, "text/plain"), - ), - ) - } - - server.addPrompt( - name = "test-prompt", - description = "A test prompt for conformance testing", - arguments = listOf( - PromptArgument( - name = "arg", - description = "Test argument", - required = false, - ), - ), - ) { - GetPromptResult( - messages = listOf( - PromptMessage( - role = Role.User, - content = TextContent("Test prompt content"), - ), - ), - description = "Test prompt description", - ) - } - - return server -} - -private class HttpServerTransport(private val sessionId: String) : AbstractTransport() { - private val logger = KotlinLogging.logger {} - private val pendingResponses = ConcurrentHashMap>() - private val messageQueue = Channel(MESSAGE_QUEUE_CAPACITY) - - suspend fun stream(call: ApplicationCall) { - logger.debug { "Starting SSE stream for session $sessionId" } - call.response.apply { - header("Cache-Control", "no-cache") - header("Connection", "keep-alive") - } - call.respondTextWriter(ContentType.Text.EventStream) { - try { - while (true) { - val msg = messageQueue.receiveCatching().getOrNull() ?: break - write("event: message\ndata: ${McpJson.encodeToString(msg)}\n\n") - flush() - } - } catch (e: Exception) { - logger.warn(e) { "SSE stream terminated for session $sessionId" } - } finally { - logger.debug { "SSE stream closed for session $sessionId" } - } - } - } - - suspend fun handleRequest(call: ApplicationCall, requestBody: JsonElement) { - try { - val message = McpJson.decodeFromJsonElement(requestBody) - logger.debug { "Handling ${message::class.simpleName}: $requestBody" } - - when (message) { - is JSONRPCRequest -> { - val idKey = when (val id = message.id) { - is RequestId.NumberId -> id.value.toString() - is RequestId.StringId -> id.value - } - val responseDeferred = CompletableDeferred() - pendingResponses[idKey] = responseDeferred - - _onMessage.invoke(message) - - val response = withTimeoutOrNull(REQUEST_TIMEOUT_MS) { responseDeferred.await() } - if (response != null) { - call.respondText(McpJson.encodeToString(response), ContentType.Application.Json) - } else { - pendingResponses.remove(idKey) - logger.warn { "Timeout for request $idKey" } - call.respondText( - McpJson.encodeToString( - JSONRPCError( - message.id, - RPCError(RPCError.ErrorCode.REQUEST_TIMEOUT, "Request timed out"), - ), - ), - ContentType.Application.Json, - ) - } - } - - else -> { - call.respond(HttpStatusCode.Accepted) - } - } - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - logger.error(e) { "Error handling request" } - if (!call.response.isCommitted) { - call.respondText( - McpJson.encodeToString( - JSONRPCError( - RequestId(0), - RPCError(RPCError.ErrorCode.INTERNAL_ERROR, "Internal error: ${e.message}"), - ), - ), - ContentType.Application.Json, - HttpStatusCode.InternalServerError, - ) - } - } - } - - override suspend fun start() { - logger.debug { "Started transport for session $sessionId" } - } - - override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { - when (message) { - is JSONRPCResponse -> { - val idKey = when (val id = message.id) { - is RequestId.NumberId -> id.value.toString() - is RequestId.StringId -> id.value - } - pendingResponses.remove(idKey)?.complete(message) ?: run { - logger.warn { "No pending response for ID $idKey, queueing" } - messageQueue.send(message) - } - } - - else -> messageQueue.send(message) - } - } - - override suspend fun close() { - logger.debug { "Closing transport for session $sessionId" } - messageQueue.close() - pendingResponses.clear() - invokeOnCloseCallback() - } -} diff --git a/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTest.kt b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTest.kt deleted file mode 100644 index 41c9c3cd3..000000000 --- a/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTest.kt +++ /dev/null @@ -1,343 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.conformance - -import io.github.oshai.kotlinlogging.KotlinLogging -import io.modelcontextprotocol.kotlin.test.utils.NPX -import io.modelcontextprotocol.kotlin.test.utils.findFreePort -import io.modelcontextprotocol.kotlin.test.utils.startLogging -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.DynamicTest -import org.junit.jupiter.api.TestFactory -import org.junit.jupiter.api.TestInstance -import java.io.BufferedReader -import java.io.InputStreamReader -import java.lang.management.ManagementFactory -import java.net.HttpURLConnection -import java.net.URI -import java.util.concurrent.TimeUnit -import kotlin.io.path.createTempFile -import kotlin.properties.Delegates -import kotlin.test.fail - -private val logger = KotlinLogging.logger {} - -val processStderrLogger = KotlinLogging.logger(name = "stderr") -val processStdoutLogger = KotlinLogging.logger(name = "stdout") - -private const val CONFORMANCE_VERSION = "0.1.8" - -enum class TransportType { - SSE, - WEBSOCKET, -} - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ConformanceTest { - - private var serverProcess: Process? = null - private var serverPort: Int by Delegates.notNull() - private val serverErrorOutput = mutableListOf() - private val maxErrorLines = 500 - - companion object { - private val SERVER_SCENARIOS = listOf( - "server-initialize", - "tools-list", - "tools-call-simple-text", - "resources-list", - "prompts-list", - // TODO: Fix - // - resources-read-text - // - prompts-get-simple - ) - - private val CLIENT_SCENARIOS = listOf( - "initialize", - // TODO: Fix - // "tools-call", - ) - - private val SERVER_TRANSPORT_TYPES = listOf( - TransportType.SSE, - // TODO: Fix -// TransportType.WEBSOCKET, - ) - - private val CLIENT_TRANSPORT_TYPES = listOf( - TransportType.SSE, - TransportType.WEBSOCKET, - ) - - private const val DEFAULT_TEST_TIMEOUT_SECONDS = 30L - private const val DEFAULT_SERVER_STARTUP_TIMEOUT_SECONDS = 10 - private const val INITIAL_BACKOFF_MS = 50L - private const val MAX_BACKOFF_MS = 500L - private const val BACKOFF_MULTIPLIER = 1.5 - private const val CONNECTION_TIMEOUT_MS = 500 - private const val GRACEFUL_SHUTDOWN_SECONDS = 5L - private const val FORCE_SHUTDOWN_SECONDS = 2L - - private fun getRuntimeClasspath(): String = ManagementFactory.getRuntimeMXBean().classPath - - private fun getTestClasspath(): String = System.getProperty("test.classpath") ?: getRuntimeClasspath() - - private fun waitForServerReady( - url: String, - timeoutSeconds: Int = DEFAULT_SERVER_STARTUP_TIMEOUT_SECONDS, - ): Boolean { - val deadline = System.currentTimeMillis() + (timeoutSeconds * 1000) - var lastError: Exception? = null - var backoffMs = INITIAL_BACKOFF_MS - - while (System.currentTimeMillis() < deadline) { - try { - val connection = URI(url).toURL().openConnection() as HttpURLConnection - connection.requestMethod = "GET" - connection.connectTimeout = CONNECTION_TIMEOUT_MS - connection.readTimeout = CONNECTION_TIMEOUT_MS - connection.connect() - - val responseCode = connection.responseCode - connection.disconnect() - logger.debug { "Server responded with code: $responseCode" } - return true - } catch (e: Exception) { - lastError = e - Thread.sleep(backoffMs) - backoffMs = (backoffMs * BACKOFF_MULTIPLIER).toLong().coerceAtMost(MAX_BACKOFF_MS) - } - } - - logger.error { "Server did not start within $timeoutSeconds seconds. Last error: ${lastError?.message}" } - return false - } - } - - @BeforeAll - fun startServer() { - serverPort = findFreePort() - val serverUrl = "http://127.0.0.1:$serverPort/mcp" - - logger.info { "Starting conformance test server on port $serverPort" } - - val processBuilder = ProcessBuilder( - "java", - "-cp", - getRuntimeClasspath(), - "io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceServerKt", - serverPort.toString(), - ) - - val process = processBuilder.start() - serverProcess = process - - // capture stderr in the background - Thread { - try { - BufferedReader(InputStreamReader(process.errorStream)).use { reader -> - reader.lineSequence().forEach { line -> - synchronized(serverErrorOutput) { - if (serverErrorOutput.size >= maxErrorLines) { - serverErrorOutput.removeAt(0) - } - serverErrorOutput.add(line) - } - logger.debug { "Server stderr: $line" } - } - } - } catch (e: Exception) { - logger.trace(e) { "Error reading server stderr" } - } - }.apply { - name = "server-stderr-reader" - isDaemon = true - }.start() - - logger.info { "Waiting for server to start..." } - val serverReady = waitForServerReady(serverUrl) - - if (!serverReady) { - val errorInfo = synchronized(serverErrorOutput) { - if (serverErrorOutput.isNotEmpty()) { - "\n\nServer error output:\n${serverErrorOutput.joinToString("\n")}" - } else { - "" - } - } - serverProcess?.destroyForcibly() - throw IllegalStateException( - "Server failed to start within $DEFAULT_SERVER_STARTUP_TIMEOUT_SECONDS seconds. " + - "Check if port $serverPort is available.$errorInfo", - ) - } - - logger.info { "Server started successfully at $serverUrl" } - } - - @AfterAll - fun stopServer() { - serverProcess?.also { process -> - logger.info { "Stopping conformance test server (PID: ${process.pid()})" } - - try { - process.destroy() - val terminated = process.waitFor(GRACEFUL_SHUTDOWN_SECONDS, TimeUnit.SECONDS) - - if (!terminated) { - logger.warn { "Server did not terminate gracefully, forcing shutdown..." } - process.destroyForcibly() - process.waitFor(FORCE_SHUTDOWN_SECONDS, TimeUnit.SECONDS) - } else { - logger.info { "Server stopped gracefully" } - } - } catch (e: Exception) { - logger.error(e) { "Error stopping server process" } - } finally { - serverProcess = null - } - } ?: logger.debug { "No server process to stop" } - } - - @TestFactory - fun `MCP Server Conformance Tests`(): List = SERVER_TRANSPORT_TYPES.flatMap { transportType -> - SERVER_SCENARIOS.map { scenario -> - DynamicTest.dynamicTest("Server [$transportType]: $scenario") { - runServerConformanceTest(scenario, transportType) - } - } - } - - @TestFactory - fun `MCP Client Conformance Tests`(): List = CLIENT_TRANSPORT_TYPES.flatMap { transportType -> - CLIENT_SCENARIOS.map { scenario -> - DynamicTest.dynamicTest("Client [$transportType]: $scenario") { - runClientConformanceTest(scenario, transportType) - } - } - } - - private fun runServerConformanceTest(scenario: String, transportType: TransportType) { - val serverUrl = when (transportType) { - TransportType.SSE -> { - "http://127.0.0.1:$serverPort/mcp" - } - - TransportType.WEBSOCKET -> { - "ws://127.0.0.1:$serverPort/ws" - } - } - - val processBuilder = ProcessBuilder( - NPX, - "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION", - "server", - "--url", - serverUrl, - "--scenario", - scenario, - ) - - runConformanceTest("server", scenario, processBuilder, transportType) - } - - private fun runClientConformanceTest(scenario: String, transportType: TransportType) { - val testClasspath = getTestClasspath() - - // Create an argfile to avoid Windows command line length limits - val argFile = createTempFile(suffix = ".args").toFile() - argFile.deleteOnExit() - - val mainClass = when (transportType) { - TransportType.SSE -> { - argFile.writeText( - buildString { - appendLine("-cp") - appendLine(testClasspath) - appendLine("io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceClientKt") - }, - ) - "http://127.0.0.1:$serverPort/mcp" - } - - TransportType.WEBSOCKET -> { - argFile.writeText( - buildString { - appendLine("-cp") - appendLine(testClasspath) - appendLine("io.modelcontextprotocol.kotlin.sdk.conformance.WebSocketConformanceClientKt") - }, - ) - "ws://127.0.0.1:$serverPort/ws" - } - } - - val clientCommand = listOf( - "java", - "@${argFile.absolutePath}", - mainClass, - ) - - val processBuilder = ProcessBuilder( - NPX, - "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION", - "client", - "--command", - clientCommand.joinToString(" "), - "--scenario", - scenario, - ) - - runConformanceTest("client", scenario, processBuilder, transportType) - } - - private fun runConformanceTest( - type: String, - scenario: String, - processBuilder: ProcessBuilder, - transportType: TransportType, - ) { - val capitalizedType = type.replaceFirstChar { it.uppercase() } - logger.info { "Running $type conformance test [$transportType]: $scenario" } - - val timeoutSeconds = - System.getenv("CONFORMANCE_TEST_TIMEOUT_SECONDS")?.toLongOrNull() ?: DEFAULT_TEST_TIMEOUT_SECONDS - - val process = processBuilder.start() - - process.errorStream.startLogging( - logger = processStderrLogger, - name = "test(PID=${process.pid()})", - ) - process.inputStream.startLogging( - logger = processStdoutLogger, - name = "test(PID=${process.pid()})", - ) - - val completed = process.waitFor(timeoutSeconds, TimeUnit.SECONDS) - - if (!completed) { - logger.error { - "$capitalizedType conformance test [$transportType] '$scenario' timed out after $timeoutSeconds seconds" - } - process.destroyForcibly() - throw AssertionError( - "❌ $capitalizedType conformance test [$transportType] '$scenario' " + - "timed out after $timeoutSeconds seconds", - ) - } - - when (val exitCode = process.exitValue()) { - 0 -> logger.info { "✅ $capitalizedType conformance test [$transportType] '$scenario' passed!" } - - else -> { - logger.error { - "$capitalizedType conformance test [$transportType] '$scenario' failed with exit code: $exitCode" - } - fail( - "❌ $capitalizedType conformance test [$transportType] '$scenario' " + - "failed (exit code: $exitCode). Check test output above for details.", - ) - } - } - } -} diff --git a/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/WebSocketConformanceClient.kt b/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/WebSocketConformanceClient.kt deleted file mode 100644 index f385dddbc..000000000 --- a/conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/WebSocketConformanceClient.kt +++ /dev/null @@ -1,105 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.conformance - -import io.github.oshai.kotlinlogging.KotlinLogging -import io.ktor.client.HttpClient -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.websocket.WebSockets -import io.ktor.client.plugins.websocket.webSocket -import io.ktor.websocket.WebSocketSession -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.shared.MCP_SUBPROTOCOL -import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport -import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest -import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams -import io.modelcontextprotocol.kotlin.sdk.types.Implementation -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject - -private val logger = KotlinLogging.logger {} - -class WebSocketClientTransport(override val session: WebSocketSession) : WebSocketMcpTransport() { - override suspend fun initializeSession() { - logger.debug { "WebSocket client session initialized" } - } -} - -@Suppress("LongMethod") -fun main(args: Array) { - require(args.isNotEmpty()) { - "Server WebSocket URL must be provided as an argument" - } - - val serverUrl = args.last() - logger.info { "Connecting to WebSocket test server at: $serverUrl" } - - val httpClient = HttpClient(CIO) { - install(WebSockets) - } - - var exitCode = 0 - - runBlocking { - try { - httpClient.webSocket(serverUrl, request = { - headers.append("Sec-WebSocket-Protocol", MCP_SUBPROTOCOL) - }) { - val transport = WebSocketClientTransport(this) - - val client = Client( - clientInfo = Implementation( - name = "kotlin-conformance-client-websocket", - version = "1.0.0", - ), - ) - - try { - client.connect(transport) - logger.info { "✅ Connected to server successfully" } - - try { - val tools = client.listTools() - logger.info { "Available tools: ${tools.tools.map { it.name }}" } - - if (tools.tools.isNotEmpty()) { - val toolName = tools.tools.first().name - logger.info { "Calling tool: $toolName" } - - val result = client.callTool( - CallToolRequest( - params = CallToolRequestParams( - name = toolName, - arguments = buildJsonObject { - put("input", JsonPrimitive("test")) - }, - ), - ), - ) - logger.info { "Tool result: ${result.content}" } - } - } catch (e: Exception) { - logger.debug(e) { "Error during tool operations (may be expected for some scenarios)" } - } - - logger.info { "✅ Client operations completed successfully" } - } catch (e: Exception) { - logger.error(e) { "❌ Client failed" } - exitCode = 1 - } finally { - try { - transport.close() - } catch (e: Exception) { - logger.warn(e) { "Error closing transport" } - } - } - } - } catch (e: Exception) { - logger.error(e) { "❌ WebSocket connection failed" } - exitCode = 1 - } finally { - httpClient.close() - } - } - - kotlin.system.exitProcess(exitCode) -} diff --git a/conformance-test/src/test/resources/simplelogger.properties b/conformance-test/src/test/resources/simplelogger.properties deleted file mode 100644 index c6f3b90c7..000000000 --- a/conformance-test/src/test/resources/simplelogger.properties +++ /dev/null @@ -1,10 +0,0 @@ -# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) -org.slf4j.simpleLogger.defaultLogLevel=INFO -org.slf4j.simpleLogger.showThreadName=true -org.slf4j.simpleLogger.showDateTime=false - -# Log level for specific packages or classes -org.slf4j.simpleLogger.log.io.ktor.server=DEBUG -org.slf4j.simpleLogger.log.stdout=TRACE -org.slf4j.simpleLogger.log.stderr=TRACE -org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f86685d37..313c8f6a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- # Ktor ktor-client-apache5 = { group = "io.ktor", name = "ktor-client-apache5", version.ref = "ktor" } +ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktor" } ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" } diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index 268a2b8df..da0e752a7 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -62,6 +62,18 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/KtorClientKt { 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/ReconnectionOptions { + public synthetic fun (JJDIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJDILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getInitialReconnectionDelay-UwyO8pc ()J + public final fun getMaxReconnectionDelay-UwyO8pc ()J + public final fun getMaxRetries ()I + public final fun getReconnectionDelayMultiplier ()D + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport { 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 @@ -88,6 +100,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTranspor } public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport { + public fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;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;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 final fun getProtocolVersion ()Ljava/lang/String; @@ -106,8 +120,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpError } public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensionsKt { + public static final fun mcpStreamableHttp (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun mcpStreamableHttp$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; 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 (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; + public static synthetic fun mcpStreamableHttpTransport$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport; 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; } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions.kt new file mode 100644 index 000000000..95c5bdefa --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions.kt @@ -0,0 +1,44 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * Options for controlling SSE reconnection behavior. + * + * @property initialReconnectionDelay The initial delay before the first reconnection attempt. + * @property maxReconnectionDelay The maximum delay between reconnection attempts. + * @property reconnectionDelayMultiplier The factor by which the delay grows on each attempt. + * @property maxRetries The maximum number of reconnection attempts per disconnect. + */ +public class ReconnectionOptions( + public val initialReconnectionDelay: Duration = 1.seconds, + public val maxReconnectionDelay: Duration = 30.seconds, + public val reconnectionDelayMultiplier: Double = 1.5, + public val maxRetries: Int = 2, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as ReconnectionOptions + + if (reconnectionDelayMultiplier != other.reconnectionDelayMultiplier) return false + if (maxRetries != other.maxRetries) return false + if (initialReconnectionDelay != other.initialReconnectionDelay) return false + if (maxReconnectionDelay != other.maxReconnectionDelay) return false + + return true + } + + override fun hashCode(): Int { + var result = reconnectionDelayMultiplier.hashCode() + result = 31 * result + maxRetries + result = 31 * result + initialReconnectionDelay.hashCode() + result = 31 * result + maxReconnectionDelay.hashCode() + return result + } + + override fun toString(): String = + "ReconnectionOptions(initialReconnectionDelay=$initialReconnectionDelay, maxReconnectionDelay=$maxReconnectionDelay, reconnectionDelayMultiplier=$reconnectionDelayMultiplier, maxRetries=$maxRetries)" +} diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt index a3b41906e..80f37aedc 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt @@ -38,9 +38,13 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.math.pow import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds private const val MCP_SESSION_ID_HEADER = "mcp-session-id" private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" @@ -52,31 +56,58 @@ private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID" public class StreamableHttpError(public val code: Int? = null, message: String? = null) : Exception("Streamable HTTP error: $message") +private sealed interface ConnectResult { + data class Success(val session: ClientSSESession) : ConnectResult + data object NonRetryable : ConnectResult + data object Failed : ConnectResult +} + /** * Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. * It will connect to a server using HTTP POST for sending messages and HTTP GET with Server-Sent Events * for receiving messages. */ -@OptIn(ExperimentalAtomicApi::class) +@Suppress("TooManyFunctions") public class StreamableHttpClientTransport( private val client: HttpClient, private val url: String, - private val reconnectionTime: Duration? = null, + private val reconnectionOptions: ReconnectionOptions = ReconnectionOptions(), private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, ) : AbstractClientTransport() { + @Deprecated( + "Use constructor with ReconnectionOptions", + replaceWith = ReplaceWith( + "StreamableHttpClientTransport(client, url, " + + "ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), requestBuilder)", + "kotlin.time.Duration.Companion.seconds", + "io.modelcontextprotocol.kotlin.sdk.client.ReconnectionOptions", + ), + ) + public constructor( + client: HttpClient, + url: String, + reconnectionTime: Duration?, + requestBuilder: HttpRequestBuilder.() -> Unit = {}, + ) : this(client, url, ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), requestBuilder) + override val logger: KLogger = KotlinLogging.logger {} public var sessionId: String? = null private set public var protocolVersion: String? = null - private var sseSession: ClientSSESession? = null private var sseJob: Job? = null private val scope by lazy { CoroutineScope(SupervisorJob() + Dispatchers.Default) } - private var lastEventId: String? = null + /** Result of an SSE stream collection. Reconnect when [hasPrimingEvent] is true and [receivedResponse] is false. */ + private data class SseStreamResult( + val hasPrimingEvent: Boolean, + val receivedResponse: Boolean, + val lastEventId: String? = null, + val serverRetryDelay: Duration? = null, + ) override suspend fun initialize() { logger.debug { "Client transport is starting..." } @@ -85,7 +116,7 @@ public class StreamableHttpClientTransport( /** * Sends a single message with optional resumption support */ - @Suppress("ReturnCount", "CyclomaticComplexMethod") + @Suppress("ReturnCount", "CyclomaticComplexMethod", "LongMethod", "TooGenericExceptionCaught", "ThrowsCount") override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { logger.debug { "Client sending message via POST to $url: ${McpJson.encodeToString(message)}" } @@ -133,18 +164,25 @@ public class StreamableHttpClientTransport( } } - ContentType.Text.EventStream -> handleInlineSse( - response, - onResumptionToken = options?.onResumptionToken, - replayMessageId = if (message is JSONRPCRequest) message.id else null, - ) + ContentType.Text.EventStream -> { + val replayMessageId = if (message is JSONRPCRequest) message.id else null + val result = handleInlineSse(response, replayMessageId, options?.onResumptionToken) + if (result.hasPrimingEvent && !result.receivedResponse) { + startSseSession( + resumptionToken = result.lastEventId, + replayMessageId = replayMessageId, + onResumptionToken = options?.onResumptionToken, + initialServerRetryDelay = result.serverRetryDelay, + ) + } + } else -> { val body = response.bodyAsText() if (response.contentType() == null && body.isBlank()) return val ct = response.contentType()?.toString() ?: "" - val error = StreamableHttpError(-1, "Unexpected content type: $$ct") + val error = StreamableHttpError(-1, "Unexpected content type: $ct") _onError(error) throw error } @@ -169,11 +207,6 @@ public class StreamableHttpClientTransport( override suspend fun closeResources() { logger.debug { "Client transport closing." } - - // Try to terminate session if we have one - terminateSession() - - sseSession?.cancel() sseJob?.cancelAndJoin() scope.cancel() } @@ -201,55 +234,120 @@ public class StreamableHttpClientTransport( } sessionId = null - lastEventId = null logger.debug { "Session terminated successfully" } } - private suspend fun startSseSession( + private fun startSseSession( resumptionToken: String? = null, replayMessageId: RequestId? = null, onResumptionToken: ((String) -> Unit)? = null, + initialServerRetryDelay: Duration? = null, ) { - sseSession?.cancel() - sseJob?.cancelAndJoin() + // Cancel-and-replace: cancel() signals the previous job, join() inside + // the new coroutine ensures it completes before we start collecting. + // This is intentionally non-suspend to avoid blocking performSend. + val previousJob = sseJob + previousJob?.cancel() + sseJob = scope.launch(CoroutineName("StreamableHttpTransport.collect#${hashCode()}")) { + previousJob?.join() + var lastEventId = resumptionToken + var serverRetryDelay = initialServerRetryDelay + var attempt = 0 + var needsDelay = initialServerRetryDelay != null + + @Suppress("LoopWithTooManyJumpStatements") + while (isActive) { + // Delay before (re)connection: skip only for first fresh SSE connection + if (needsDelay) { + delay(getNextReconnectionDelay(attempt, serverRetryDelay)) + } + needsDelay = true + + // Connect + val session = when (val cr = connectSse(lastEventId)) { + is ConnectResult.Success -> { + attempt = 0 + cr.session + } + + ConnectResult.NonRetryable -> return@launch + + ConnectResult.Failed -> { + // Give up after maxRetries consecutive failed connection attempts + if (++attempt >= reconnectionOptions.maxRetries) { + _onError(StreamableHttpError(null, "Maximum reconnection attempts exceeded")) + return@launch + } + continue + } + } + + // Collect + val result = collectSse(session, replayMessageId, onResumptionToken) + lastEventId = result.lastEventId ?: lastEventId + serverRetryDelay = result.serverRetryDelay ?: serverRetryDelay + if (result.receivedResponse) break + } + } + } + @Suppress("TooGenericExceptionCaught") + private suspend fun connectSse(lastEventId: String?): ConnectResult { logger.debug { "Client attempting to start SSE session at url: $url" } - try { - sseSession = client.sseSession( - urlString = url, - reconnectionTime = reconnectionTime, - ) { + return try { + val session = client.sseSession(urlString = url, showRetryEvents = true) { method = HttpMethod.Get applyCommonHeaders(this) - // sseSession will add ContentType.Text.EventStream automatically accept(ContentType.Application.Json) - (resumptionToken ?: lastEventId)?.let { headers.append(MCP_RESUMPTION_TOKEN_HEADER, it) } + lastEventId?.let { headers.append(MCP_RESUMPTION_TOKEN_HEADER, it) } requestBuilder() } logger.debug { "Client SSE session started successfully." } + ConnectResult.Success(session) + } catch (e: CancellationException) { + throw e } catch (e: SSEClientException) { - val responseStatus = e.response?.status - val responseContentType = e.response?.contentType() + if (isNonRetryableSseError(e)) { + ConnectResult.NonRetryable + } else { + logger.debug { "SSE connection failed: ${e.message}" } + ConnectResult.Failed + } + } catch (e: Exception) { + logger.debug { "SSE connection failed: ${e.message}" } + ConnectResult.Failed + } + } - // 405 means server doesn't support SSE at GET endpoint - this is expected and valid - if (responseStatus == HttpStatusCode.MethodNotAllowed) { - logger.info { "Server returned 405 for GET/SSE, stream disabled." } - return + private fun getNextReconnectionDelay(attempt: Int, serverRetryDelay: Duration?): Duration { + // Per SSE specification, the server-sent `retry` field sets the reconnection time + // for all subsequent attempts, taking priority over exponential backoff. + serverRetryDelay?.let { return it } + val delay = reconnectionOptions.initialReconnectionDelay * + reconnectionOptions.reconnectionDelayMultiplier.pow(attempt) + return delay.coerceAtMost(reconnectionOptions.maxReconnectionDelay) + } + + /** + * Checks if an SSE session error is non-retryable (404, 405, JSON-only). + * Returns `true` if non-retryable (should stop trying), `false` otherwise. + */ + private fun isNonRetryableSseError(e: SSEClientException): Boolean { + val responseStatus = e.response?.status + val responseContentType = e.response?.contentType() + + return when { + responseStatus == HttpStatusCode.NotFound || responseStatus == HttpStatusCode.MethodNotAllowed -> { + logger.info { "Server returned ${responseStatus.value} for GET/SSE, stream disabled." } + true } - // If server returns application/json, it means it doesn't support SSE for this session - // This is valid per spec - server can choose to only use JSON responses - if (responseContentType?.match(ContentType.Application.Json) == true) { + responseContentType?.match(ContentType.Application.Json) == true -> { logger.info { "Server returned application/json for GET/SSE, using JSON-only mode." } - return + true } - _onError(e) - throw e - } - - sseJob = scope.launch(CoroutineName("StreamableHttpTransport.collect#${hashCode()}")) { - sseSession?.let { collectSse(it, replayMessageId, onResumptionToken) } + else -> false } } @@ -265,11 +363,17 @@ public class StreamableHttpClientTransport( session: ClientSSESession, replayMessageId: RequestId?, onResumptionToken: ((String) -> Unit)?, - ) { + ): SseStreamResult { + var hasPrimingEvent = false + var receivedResponse = false + var localLastEventId: String? = null + var localServerRetryDelay: Duration? = null try { session.incoming.collect { event -> + event.retry?.let { localServerRetryDelay = it.milliseconds } event.id?.let { - lastEventId = it + localLastEventId = it + hasPrimingEvent = true onResumptionToken?.invoke(it) } logger.trace { "Client received SSE event: event=${event.event}, data=${event.data}, id=${event.id}" } @@ -278,6 +382,7 @@ public class StreamableHttpClientTransport( event.data?.takeIf { it.isNotEmpty() }?.let { json -> runCatching { McpJson.decodeFromString(json) } .onSuccess { msg -> + if (msg is JSONRPCResponse) receivedResponse = true if (replayMessageId != null && msg is JSONRPCResponse) { _onMessage(msg.copy(id = replayMessageId)) } else { @@ -295,6 +400,7 @@ public class StreamableHttpClientTransport( } catch (t: Throwable) { _onError(t) } + return SseStreamResult(hasPrimingEvent, receivedResponse, localLastEventId, localServerRetryDelay) } @Suppress("CyclomaticComplexMethod") @@ -302,17 +408,22 @@ public class StreamableHttpClientTransport( response: HttpResponse, replayMessageId: RequestId?, onResumptionToken: ((String) -> Unit)?, - ) { + ): SseStreamResult { logger.trace { "Handling inline SSE from POST response" } val channel = response.bodyAsChannel() + var hasPrimingEvent = false + var receivedResponse = false + var localLastEventId: String? = null + var localServerRetryDelay: Duration? = null val sb = StringBuilder() var id: String? = null var eventName: String? = null suspend fun dispatch(id: String?, eventName: String?, data: String) { id?.let { - lastEventId = it + localLastEventId = it + hasPrimingEvent = true onResumptionToken?.invoke(it) } if (data.isBlank()) { @@ -321,6 +432,7 @@ public class StreamableHttpClientTransport( if (eventName == null || eventName == "message") { runCatching { McpJson.decodeFromString(data) } .onSuccess { msg -> + if (msg is JSONRPCResponse) receivedResponse = true if (replayMessageId != null && msg is JSONRPCResponse) { _onMessage(msg.copy(id = replayMessageId)) } else { @@ -351,9 +463,16 @@ public class StreamableHttpClientTransport( } when { line.startsWith("id:") -> id = line.substringAfter("id:").trim() + line.startsWith("event:") -> eventName = line.substringAfter("event:").trim() + line.startsWith("data:") -> sb.append(line.substringAfter("data:").trim()) + + line.startsWith("retry:") -> line.substringAfter("retry:").trim().toLongOrNull()?.let { + localServerRetryDelay = it.milliseconds + } } } + return SseStreamResult(hasPrimingEvent, receivedResponse, localLastEventId, localServerRetryDelay) } } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt index b64a22062..a618f1823 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt @@ -6,21 +6,67 @@ import io.modelcontextprotocol.kotlin.sdk.LIB_VERSION import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME import io.modelcontextprotocol.kotlin.sdk.types.Implementation import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds /** * Returns a new Streamable HTTP transport for the Model Context Protocol using the provided HttpClient. * * @param url URL of the MCP server. - * @param reconnectionTime Optional duration to wait before attempting to reconnect. + * @param reconnectionOptions Options for controlling SSE reconnection behavior. * @param requestBuilder Optional lambda to configure the HTTP request. * @return A [StreamableHttpClientTransport] configured for MCP communication. */ public fun HttpClient.mcpStreamableHttpTransport( url: String, - reconnectionTime: Duration? = null, + reconnectionOptions: ReconnectionOptions = ReconnectionOptions(), requestBuilder: HttpRequestBuilder.() -> Unit = {}, ): StreamableHttpClientTransport = - StreamableHttpClientTransport(this, url, reconnectionTime, requestBuilder = requestBuilder) + StreamableHttpClientTransport(this, url, reconnectionOptions, requestBuilder = requestBuilder) + +/** + * Returns a new Streamable HTTP transport for the Model Context Protocol using the provided HttpClient. + * + * @param url URL of the MCP server. + * @param reconnectionTime Optional duration to wait before attempting to reconnect. + * @param requestBuilder Optional lambda to configure the HTTP request. + * @return A [StreamableHttpClientTransport] configured for MCP communication. + */ +@Deprecated( + "Use overload with ReconnectionOptions", + replaceWith = ReplaceWith( + "mcpStreamableHttpTransport(url, " + + "ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), requestBuilder)", + ), +) +public fun HttpClient.mcpStreamableHttpTransport( + url: String, + reconnectionTime: Duration?, + requestBuilder: HttpRequestBuilder.() -> Unit = {}, +): StreamableHttpClientTransport = StreamableHttpClientTransport( + this, + url, + ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), + requestBuilder = requestBuilder, +) + +/** + * Creates and connects an MCP client over Streamable HTTP using the provided HttpClient. + * + * @param url URL of the MCP server. + * @param reconnectionOptions Options for controlling SSE reconnection behavior. + * @param requestBuilder Optional lambda to configure the HTTP request. + * @return A connected [Client] ready for MCP communication. + */ +public suspend fun HttpClient.mcpStreamableHttp( + url: String, + reconnectionOptions: ReconnectionOptions = ReconnectionOptions(), + requestBuilder: HttpRequestBuilder.() -> Unit = {}, +): Client { + val transport = mcpStreamableHttpTransport(url, reconnectionOptions, requestBuilder) + val client = Client(Implementation(name = IMPLEMENTATION_NAME, version = LIB_VERSION)) + client.connect(transport) + return client +} /** * Creates and connects an MCP client over Streamable HTTP using the provided HttpClient. @@ -30,12 +76,23 @@ public fun HttpClient.mcpStreamableHttpTransport( * @param requestBuilder Optional lambda to configure the HTTP request. * @return A connected [Client] ready for MCP communication. */ +@Deprecated( + "Use overload with ReconnectionOptions", + replaceWith = ReplaceWith( + "mcpStreamableHttp(url, " + + "ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), requestBuilder)", + ), +) public suspend fun HttpClient.mcpStreamableHttp( url: String, - reconnectionTime: Duration? = null, + reconnectionTime: Duration?, requestBuilder: HttpRequestBuilder.() -> Unit = {}, ): Client { - val transport = mcpStreamableHttpTransport(url, reconnectionTime, requestBuilder) + val transport = mcpStreamableHttpTransport( + url, + ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), + requestBuilder, + ) val client = Client(Implementation(name = IMPLEMENTATION_NAME, version = LIB_VERSION)) client.connect(transport) return client diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt index e303326ff..55df3e06e 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt @@ -23,9 +23,11 @@ import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse import io.modelcontextprotocol.kotlin.sdk.types.McpException import io.modelcontextprotocol.kotlin.sdk.types.McpJson import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.RequestId import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -657,6 +659,203 @@ class StreamableHttpClientTransportTest { receivedErrors shouldHaveSize 0 } + @Test + fun testInlineSseRetryParsing() = runTest { + val transport = createTransport { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + appendLine("retry: 5000") + appendLine("id: ev-1") + appendLine("event: message") + appendLine("""data: {"jsonrpc":"2.0","id":"req-1","result":{"tools":[]}}""") + appendLine() + } + + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val responseReceived = CompletableDeferred() + + transport.onMessage { message -> + receivedMessages.add(message) + if (message is JSONRPCResponse && !responseReceived.isCompleted) { + responseReceived.complete(Unit) + } + } + + transport.start() + + transport.send( + JSONRPCRequest( + id = "req-1", + method = "test", + params = buildJsonObject { }, + ), + ) + + eventually { + responseReceived.await() + } + + receivedMessages shouldHaveSize 1 + val response = receivedMessages[0] as JSONRPCResponse + response.id shouldBe RequestId.StringId("req-1") + + transport.close() + } + + @Test + fun testInlineSseHasPrimingEventTracking() = runTest { + val transport = createTransport { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + // Event with id = priming event + appendLine("id: priming-1") + appendLine("event: message") + appendLine( + """data: {"jsonrpc":"2.0","method":"notifications/progress",""" + + """"params":{"progressToken":"t1","progress":50}}""", + ) + appendLine() + // Notification without id + appendLine("event: message") + appendLine("""data: {"jsonrpc":"2.0","method":"notifications/tools/list_changed"}""") + appendLine() + } + + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val twoMessagesReceived = CompletableDeferred() + + transport.onMessage { message -> + receivedMessages.add(message) + if (receivedMessages.size >= 2 && !twoMessagesReceived.isCompleted) { + twoMessagesReceived.complete(Unit) + } + } + + transport.start() + + transport.send( + JSONRPCRequest( + id = "test-1", + method = "test", + params = buildJsonObject { }, + ), + ) + + eventually { + twoMessagesReceived.await() + } + + receivedMessages shouldHaveSize 2 + // Both should be notifications (no JSONRPCResponse → POST-to-GET reconnect would be triggered) + receivedMessages[0].shouldBeInstanceOf() + receivedMessages[1].shouldBeInstanceOf() + + transport.close() + } + + @Test + fun testInlineSseResponseStopsReconnection() = runTest { + val transport = createTransport { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + appendLine("id: ev-1") + appendLine("event: message") + appendLine("""data: {"jsonrpc":"2.0","id":"req-1","result":{"tools":[]}}""") + appendLine() + } + + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val responseReceived = CompletableDeferred() + + transport.onMessage { message -> + receivedMessages.add(message) + if (message is JSONRPCResponse && !responseReceived.isCompleted) { + responseReceived.complete(Unit) + } + } + + transport.start() + + transport.send( + JSONRPCRequest( + id = "req-1", + method = "tools/list", + params = buildJsonObject { }, + ), + ) + + eventually { + responseReceived.await() + } + + receivedMessages shouldHaveSize 1 + // Response received → no reconnection triggered (hasPrimingEvent=true, receivedResponse=true) + val response = receivedMessages[0] as JSONRPCResponse + response.id shouldBe RequestId.StringId("req-1") + + transport.close() + } + + @Suppress("DEPRECATION") + @Test + fun testDeprecatedConstructorStillWorks() = runTest { + val mockEngine = MockEngine { _ -> + respond( + content = "", + status = HttpStatusCode.Accepted, + ) + } + val httpClient = HttpClient(mockEngine) { + install(SSE) + } + + val transport = + StreamableHttpClientTransport(httpClient, url = "http://localhost:8080/mcp", reconnectionTime = 2.seconds) + + transport.start() + transport.send(JSONRPCNotification(method = "test")) + transport.close() + } + private suspend fun setupTransportAndCollectMessages( transport: StreamableHttpClientTransport, ): Pair, MutableList> { diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt index 6513b469b..a1fe89e66 100644 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.client import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.shouldBe import io.ktor.http.ContentType import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode @@ -203,7 +204,31 @@ internal class StreamableHttpClientTest : AbstractStreamableHttpClientTest() { meta = EmptyJsonObject, ) + client.close() + } + + @Test + fun `terminateSession sends DELETE request`() = runBlocking { + val client = Client( + clientInfo = Implementation(name = "client1", version = "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + val sessionId = Uuid.random().toString() + + mockMcp.onInitialize(clientName = "client1", sessionId = sessionId) + mockMcp.handleJSONRPCRequest( + jsonRpcMethod = "notifications/initialized", + expectedSessionId = sessionId, + sessionId = sessionId, + statusCode = HttpStatusCode.Accepted, + ) + mockMcp.handleSubscribeWithGet(sessionId) { emptyFlow() } + + connect(client) + mockMcp.mockUnsubscribeRequest(sessionId = sessionId) + (client.transport as StreamableHttpClientTransport).terminateSession() + (client.transport as StreamableHttpClientTransport).sessionId shouldBe null client.close() } @@ -257,8 +282,6 @@ internal class StreamableHttpClientTest : AbstractStreamableHttpClientTest() { buildJsonObject {} } - mockMcp.mockUnsubscribeRequest(sessionId = sessionId) - connect(client) delay(1.seconds) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt index 5e39d1ffb..8ddee0a9c 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt @@ -8,12 +8,13 @@ import kotlinx.serialization.json.JsonObject // Protocol Version Constants // ============================================================================ -public const val LATEST_PROTOCOL_VERSION: String = "2025-06-18" +public const val LATEST_PROTOCOL_VERSION: String = "2025-11-25" -public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-03-26" +public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-06-18" public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( LATEST_PROTOCOL_VERSION, + "2025-06-18", "2025-03-26", "2024-11-05", ) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt index a00b26be4..4d5974d92 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt @@ -371,21 +371,23 @@ internal object ServerResultPolymorphicSerializer : * Polymorphic serializer for [JSONRPCMessage] types. * Determines the message type based on the presence of specific fields: * - "error" -> JSONRPCError - * - "result" -> JSONRPCResponse + * - "result" + "id" -> JSONRPCResponse + * - "result" -> JSONRPCEmptyMessage * - "method" + "id" -> JSONRPCRequest * - "method" -> JSONRPCNotification */ internal object JSONRPCMessagePolymorphicSerializer : JsonContentPolymorphicSerializer(JSONRPCMessage::class) { override fun selectDeserializer(element: JsonElement): DeserializationStrategy { - val jsonObject = element.jsonObject + val jsonObj = element.jsonObject return when { - "error" in jsonObject -> JSONRPCError.serializer() - "result" in jsonObject -> JSONRPCResponse.serializer() - "method" in jsonObject && "id" in jsonObject -> JSONRPCRequest.serializer() - "method" in jsonObject -> JSONRPCNotification.serializer() - jsonObject.isEmpty() || jsonObject.keys == setOf("jsonrpc") -> JSONRPCEmptyMessage.serializer() - else -> throw SerializationException("Invalid JSONRPCMessage type: ${jsonObject.keys}") + "error" in jsonObj -> JSONRPCError.serializer() + "result" in jsonObj && "id" in jsonObj -> JSONRPCResponse.serializer() + "result" in jsonObj && jsonObj["result"]?.jsonObject?.isEmpty() == true -> JSONRPCEmptyMessage.serializer() + "method" in jsonObj && "id" in jsonObj -> JSONRPCRequest.serializer() + "method" in jsonObj -> JSONRPCNotification.serializer() + jsonObj.isEmpty() || jsonObj.keys == setOf("jsonrpc") -> JSONRPCEmptyMessage.serializer() + else -> throw SerializationException("Invalid JSONRPCMessage type: ${jsonObj.keys}") } } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CommonTypeTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CommonTypeTest.kt index 44e88ac37..a29a19cde 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CommonTypeTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CommonTypeTest.kt @@ -14,13 +14,14 @@ class CommonTypeTest { @Test fun `should have correct latest protocol version`() { - LATEST_PROTOCOL_VERSION shouldBe "2025-06-18" + LATEST_PROTOCOL_VERSION shouldBe "2025-11-25" } @Test fun `should have correct supported protocol versions`() { SUPPORTED_PROTOCOL_VERSIONS shouldContainExactlyInAnyOrder listOf( LATEST_PROTOCOL_VERSION, + "2025-06-18", "2025-03-26", "2024-11-05", ) diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 580fcc0e5..9074c4c26 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -45,6 +45,7 @@ internal const val MCP_SESSION_ID_HEADER = "mcp-session-id" private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID" private const val MAXIMUM_MESSAGE_SIZE = 4 * 1024 * 1024 // 4 MB +private const val MIN_PRIMING_EVENT_PROTOCOL_VERSION = "2025-11-25" /** * A holder for an active request call. @@ -388,7 +389,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat if (!configuration.enableJsonResponse) { call.appendSseHeaders() flushSse(session) // flush headers immediately - maybeSendPrimingEvent(streamId, session) + maybeSendPrimingEvent(streamId, session, call.request.header(MCP_PROTOCOL_VERSION_HEADER)) } streamMutex.withLock { @@ -451,7 +452,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat call.appendSseHeaders() flushSse(sseSession) // flush headers immediately streamsMapping[STANDALONE_SSE_STREAM_ID] = SessionContext(sseSession, call) - maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession) + maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession, call.request.header(MCP_PROTOCOL_VERSION_HEADER)) sseSession.coroutineContext.job.invokeOnCompletion { streamsMapping.remove(STANDALONE_SSE_STREAM_ID) } @@ -702,12 +703,20 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } @Suppress("TooGenericExceptionCaught") - private suspend fun maybeSendPrimingEvent(streamId: String, session: ServerSSESession?) { - val store = configuration.eventStore ?: return - val sseSession = session ?: return + private suspend fun maybeSendPrimingEvent( + streamId: String, + session: ServerSSESession?, + clientProtocolVersion: String? = null, + ) { + val store = configuration.eventStore + if (store == null || session == null) return + // Priming events have empty data which older clients cannot handle. + // Only send priming events to clients with protocol version >= 2025-11-25 + // which includes the fix for handling empty SSE data. + if (clientProtocolVersion != null && clientProtocolVersion < MIN_PRIMING_EVENT_PROTOCOL_VERSION) return try { val primingEventId = store.storeEvent(streamId, JSONRPCEmptyMessage) - sseSession.send( + session.send( id = primingEventId, retry = configuration.retryInterval?.inWholeMilliseconds, data = "", From 736efe9dc08ae083b568c7049aca511cafe6e7b8 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:52:27 +0200 Subject: [PATCH 018/187] fix(server): notifications not delivered over Streamable HTTP (#587) (#599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes - fix(server): keep SSE connection open until explicitly cancelled - conformance: add logging to test_tool_with_logging and update troubleshooting documentation - chore(conformance): remove 5 passing server tests from conformance baselines: - tools-call-with-logging - tools-call-with-progress - tools-call-sampling - tools-call-elicitation - elicitation-sep1034-defaults ## Motivation and Context Two bugs in StreamableHttpServerTransport caused server-to-client notifications (e.g. notifications/message during tool calls) to be silently dropped. **Bug 1 — GET SSE stream closed immediately** `handleGetRequest` rejected GET requests with 405 when `enableJsonResponse = true` (which `mcpStreamableHttp` always sets). Since the Ktor `sse {}` handler commits response headers before the body runs, the reject failed silently and the function returned, causing Ktor to close the SSE stream. There was also no `awaitCancellation()` to keep the connection alive. **Bug 2 — Notifications discarded in JSON mode** In `send()`, notifications with a relatedRequestId entered the POST-stream path but hit `if (!isTerminated) return` without being forwarded anywhere. **Fix** - Remove the enableJsonResponse guard from handleGetRequest. The GET SSE stream is an always-required notification channel, orthogonal to how POST responses are delivered. - Add `awaitCancellation()` to keep the GET SSE connection open until the client disconnects or the transport closes. - In `send()`, route notifications with a relatedRequestId to the standalone GET SSE stream when `enableJsonResponse = true`. ## How Has This Been Tested? Verified against the MCP conformance test suite — the ToolsCallWithLogging scenario now passes. ngrep confirms the `GET /mcp` stream stays open and `notifications/message` events arrive before the `tools/call` response. ## Breaking Changes ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context Fixes #587 Requires #585 to be merged first. --------- Co-authored-by: devcrocod --- conformance-test/README.md | 15 ++++------- conformance-test/conformance-baseline.yml | 5 ---- .../sdk/conformance/ConformanceTools.kt | 7 ++++++ .../main/resources/simplelogger.properties | 9 +++++++ .../server/StreamableHttpServerTransport.kt | 25 ++++++++++++------- 5 files changed, 37 insertions(+), 24 deletions(-) create mode 100644 conformance-test/src/main/resources/simplelogger.properties diff --git a/conformance-test/README.md b/conformance-test/README.md index d14fcfef3..3f1e366bc 100644 --- a/conformance-test/README.md +++ b/conformance-test/README.md @@ -114,16 +114,11 @@ Tests the conformance server against all server scenarios: 8 scenarios are expected to fail due to current SDK limitations (tracked in [ `conformance-baseline.yml`](conformance-baseline.yml). -| Scenario | Suite | Root Cause | -|---------------------------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------| -| `tools-call-with-logging` | server | Notifications from tool handlers have no `relatedRequestId`; transport routes them to the standalone SSE stream instead of the request-specific stream | -| `tools-call-with-progress` | server | *(same as above)* | -| `tools-call-sampling` | server | *(same as above)* | -| `tools-call-elicitation` | server | *(same as above)* | -| `elicitation-sep1034-defaults` | server | *(same as above)* | -| `elicitation-sep1330-enums` | server | *(same as above)* | -| `resources-templates-read` | server | SDK does not implement `addResourceTemplate()` with URI pattern matching; resources are looked up by exact URI | -| `elicitation-sep1034-client-defaults` | client | SDK does not fill in `default` values from the elicitation request schema before sending the response | +| Scenario | Suite | Root Cause | +|---------------------------------------|--------|----------------------------------------------------------------------------------------------------------------| +| `elicitation-sep1330-enums` | server | *(same as above)* | +| `resources-templates-read` | server | SDK does not implement `addResourceTemplate()` with URI pattern matching; resources are looked up by exact URI | +| `elicitation-sep1034-client-defaults` | client | SDK does not fill in `default` values from the elicitation request schema before sending the response | These failures reveal SDK gaps and are intentionally not fixed in this module. diff --git a/conformance-test/conformance-baseline.yml b/conformance-test/conformance-baseline.yml index cc06a389b..c15041f42 100644 --- a/conformance-test/conformance-baseline.yml +++ b/conformance-test/conformance-baseline.yml @@ -1,11 +1,6 @@ # Conformance test baseline - expected failures # Add entries here as tests are identified as known SDK limitations server: - - tools-call-with-logging - - tools-call-with-progress - - tools-call-sampling - - tools-call-elicitation - - elicitation-sep1034-defaults - elicitation-sep1330-enums - resources-templates-read diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt index ebe7a5716..def59f81e 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt @@ -1,5 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.conformance +import io.github.oshai.kotlinlogging.KotlinLogging import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.types.AudioContent import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult @@ -33,6 +34,8 @@ internal const val PNG_BASE64 = // Minimal WAV (base64) internal const val WAV_BASE64 = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA=" +private val logger = KotlinLogging.logger {} + @Suppress("LongMethod") fun Server.registerConformanceTools() { // 1. Simple text @@ -429,6 +432,7 @@ fun Server.registerConformanceTools() { name = "test_tool_with_logging", description = "test_tool_with_logging", ) { + logger.debug { "[test_tool_with_logging] Sending message 1" } sendLoggingMessage( LoggingMessageNotification( LoggingMessageNotificationParams( @@ -439,6 +443,7 @@ fun Server.registerConformanceTools() { ), ) delay(50.milliseconds) + logger.debug { "[test_tool_with_logging] Sending message #2" } sendLoggingMessage( LoggingMessageNotification( LoggingMessageNotificationParams( @@ -449,6 +454,7 @@ fun Server.registerConformanceTools() { ), ) delay(50.milliseconds) + logger.debug { "[test_tool_with_logging] Sending message 3" } sendLoggingMessage( LoggingMessageNotification( LoggingMessageNotificationParams( @@ -458,6 +464,7 @@ fun Server.registerConformanceTools() { ), ), ) + CallToolResult(listOf(TextContent("Simple text content"))) } diff --git a/conformance-test/src/main/resources/simplelogger.properties b/conformance-test/src/main/resources/simplelogger.properties new file mode 100644 index 000000000..16141d375 --- /dev/null +++ b/conformance-test/src/main/resources/simplelogger.properties @@ -0,0 +1,9 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.ktor.server=DEBUG +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG +org.slf4j.simpleLogger.log.io.modelcontextprotocol.kotlin.sdk.conformance=INFO diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 9074c4c26..d0bbcfbbf 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -28,6 +28,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.RPCError import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.REQUEST_TIMEOUT import io.modelcontextprotocol.kotlin.sdk.types.RequestId import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.job import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -242,7 +243,15 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } val isTerminated = message is JSONRPCResponse || message is JSONRPCError - if (!isTerminated) return + if (!isTerminated) { + if (configuration.enableJsonResponse) { + // In JSON response mode there is no per-request SSE stream, so route notifications + // that are logically associated with a request to the standalone GET SSE stream. + val standaloneStream = streamsMapping[STANDALONE_SSE_STREAM_ID] + standaloneStream?.let { emitOnStream(STANDALONE_SSE_STREAM_ID, it.session, message) } + } + return + } requestToResponseMapping[responseRequestId!!] = message val relatedIds = requestToStreamMapping.filterValues { it == streamId }.keys @@ -411,14 +420,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat @Suppress("ReturnCount") public suspend fun handleGetRequest(session: ServerSSESession?, call: ApplicationCall) { - if (configuration.enableJsonResponse) { - call.reject( - HttpStatusCode.MethodNotAllowed, - RPCError.ErrorCode.CONNECTION_CLOSED, - "Method not allowed.", - ) - return - } + // NOTE: enableJsonResponse only controls how POST responses are delivered (JSON vs. SSE). + // The standalone GET SSE stream is always supported — it is the only channel available + // for server-to-client notifications when enableJsonResponse = true. val sseSession = session ?: error("Server session can't be null for streaming GET requests") val acceptHeader = call.request.header(HttpHeaders.Accept) @@ -456,6 +460,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat sseSession.coroutineContext.job.invokeOnCompletion { streamsMapping.remove(STANDALONE_SSE_STREAM_ID) } + // Keep the SSE connection open until the client disconnects or the transport is closed. + // Without this, the Ktor sse{} handler returns immediately, closing the stream. + awaitCancellation() } public suspend fun handleDeleteRequest(call: ApplicationCall) { From 4ecd95218a4b33b9551ff9e26d7cb4f1f5f35764 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:25:14 +0200 Subject: [PATCH 019/187] fix(conformance): correct SEP-1330 enum schemas in test_elicitation_sep1330_enums (#600) ## Correct SEP-1330 enum schemas in test_elicitation_sep1330_enums **NB! This PR contains changes from #599 and should be rebased and merged after #599 is merged.** Update conformance test according to [test requirements](https://github.com/modelcontextprotocol/conformance/blob/main/src/scenarios/server/elicitation-enums.ts#L14C1-L35C9). - Fix legacyEnum: was using `oneOf` with const/title pairs; now correctly uses `enum` + `enumNames` arrays per LegacyEnumSchema spec - Fix titledMulti: items were using `oneOf` with extra `type:"string"`; now correctly uses `anyOf` per TitledMultiSelectEnumSchema spec - Fix return text format to match expected "Elicitation completed: action=..., content=..." - Remove _elicitation-sep1330-enums_ from conformance baseline (test now passes) ## How Has This Been Tested? ```shell ./conformance-test/run-conformance.sh server ``` ## Breaking Changes No ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --------- Co-authored-by: devcrocod --- conformance-test/README.md | 11 ++----- conformance-test/conformance-baseline.yml | 1 - .../sdk/conformance/ConformanceTools.kt | 30 ++++++++----------- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/conformance-test/README.md b/conformance-test/README.md index 3f1e366bc..9b827bcf3 100644 --- a/conformance-test/README.md +++ b/conformance-test/README.md @@ -111,15 +111,8 @@ Tests the conformance server against all server scenarios: ## Known SDK Limitations -8 scenarios are expected to fail due to current SDK limitations (tracked in [ -`conformance-baseline.yml`](conformance-baseline.yml). - -| Scenario | Suite | Root Cause | -|---------------------------------------|--------|----------------------------------------------------------------------------------------------------------------| -| `elicitation-sep1330-enums` | server | *(same as above)* | -| `resources-templates-read` | server | SDK does not implement `addResourceTemplate()` with URI pattern matching; resources are looked up by exact URI | -| `elicitation-sep1034-client-defaults` | client | SDK does not fill in `default` values from the elicitation request schema before sending the response | - +Scenarios, expected to fail due to current SDK limitations, are tracked in +[`conformance-baseline.yml`](conformance-baseline.yml). These failures reveal SDK gaps and are intentionally not fixed in this module. [list-scenarios-command]: https://github.com/modelcontextprotocol/conformance/tree/main?tab=readme-ov-file#list-available-scenarios diff --git a/conformance-test/conformance-baseline.yml b/conformance-test/conformance-baseline.yml index c15041f42..378884748 100644 --- a/conformance-test/conformance-baseline.yml +++ b/conformance-test/conformance-baseline.yml @@ -1,7 +1,6 @@ # Conformance test baseline - expected failures # Add entries here as tests are identified as known SDK limitations server: - - elicitation-sep1330-enums - resources-templates-read client: diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt index def59f81e..579b22232 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt @@ -318,27 +318,22 @@ fun Server.registerConformanceTools() { ) }, ) - // Legacy titled (deprecated) + // Legacy titled (deprecated) - uses enum + enumNames arrays put( "legacyEnum", buildJsonObject { put("type", JsonPrimitive("string")) put( - "oneOf", + "enum", + JsonArray(listOf(JsonPrimitive("opt1"), JsonPrimitive("opt2"), JsonPrimitive("opt3"))), + ) + put( + "enumNames", JsonArray( listOf( - buildJsonObject { - put("const", JsonPrimitive("opt1")) - put("title", JsonPrimitive("Option One")) - }, - buildJsonObject { - put("const", JsonPrimitive("opt2")) - put("title", JsonPrimitive("Option Two")) - }, - buildJsonObject { - put("const", JsonPrimitive("opt3")) - put("title", JsonPrimitive("Option Three")) - }, + JsonPrimitive("Option One"), + JsonPrimitive("Option Two"), + JsonPrimitive("Option Three"), ), ), ) @@ -367,7 +362,7 @@ fun Server.registerConformanceTools() { ) }, ) - // Titled multi-select + // Titled multi-select - uses items.anyOf with const/title pairs put( "titledMulti", buildJsonObject { @@ -375,9 +370,8 @@ fun Server.registerConformanceTools() { put( "items", buildJsonObject { - put("type", JsonPrimitive("string")) put( - "oneOf", + "anyOf", JsonArray( listOf( buildJsonObject { @@ -405,7 +399,7 @@ fun Server.registerConformanceTools() { "Please review and update the form fields with defaults", schema, ) - CallToolResult(listOf(TextContent(result.content.toString()))) + CallToolResult(listOf(TextContent("Elicitation completed: action=${result.action}, content=${result.content}"))) } // 12. Dynamic tool From 921b1fd30ee5fb1a8774c652a24f55bb793ef29d Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Fri, 13 Mar 2026 10:23:51 +0100 Subject: [PATCH 020/187] docs: update client and server samples (#598) Updated the client and server examples for further use in the following guides: https://modelcontextprotocol.io/docs/develop/build-server and https://modelcontextprotocol.io/docs/develop/build-client ## How Has This Been Tested? claude desktop, cli ## Breaking Changes NaN ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Example, Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- .../gradle/libs.versions.toml | 4 +- .../sample/client/MCPClient.kt | 207 +++++++++++------- .../sample/client/main.kt | 13 +- samples/weather-stdio-server/README.md | 2 +- samples/weather-stdio-server/build.gradle.kts | 4 +- .../gradle/libs.versions.toml | 5 +- .../sample/server/McpWeatherServer.kt | 166 +++++++++----- .../sample/server/WeatherApi.kt | 132 +++++------ .../sample/client/ClientStdio.kt | 6 +- 9 files changed, 311 insertions(+), 228 deletions(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index fa8b4e976..ab34b7e65 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -anthropic = "2.9.0" +anthropic = "2.15.0" kotlin = "2.2.21" ktor = "3.2.3" -mcp-kotlin = "0.8.1" +mcp-kotlin = "0.9.0" shadow = "9.2.2" slf4j = "2.0.17" diff --git a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt index 8360316e2..23fdfcc0d 100644 --- a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt +++ b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/MCPClient.kt @@ -2,10 +2,11 @@ package io.modelcontextprotocol.sample.client import com.anthropic.client.okhttp.AnthropicOkHttpClient import com.anthropic.core.JsonValue +import com.anthropic.models.messages.ContentBlockParam import com.anthropic.models.messages.MessageCreateParams import com.anthropic.models.messages.MessageParam -import com.anthropic.models.messages.Model import com.anthropic.models.messages.Tool +import com.anthropic.models.messages.ToolResultBlockParam import com.anthropic.models.messages.ToolUnion import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper @@ -14,24 +15,28 @@ import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.yield +import kotlinx.coroutines.withContext import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered import kotlinx.serialization.json.JsonObject -import kotlin.jvm.optionals.getOrNull +import java.util.concurrent.TimeUnit -class MCPClient : AutoCloseable { - // Configures using the `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` environment variables - private val anthropic = AnthropicOkHttpClient.fromEnv() +private const val MODEL = "claude-sonnet-4-20250514" +private const val MAX_TOKENS = 1024L + +class MCPClient(apiKey: String) : AutoCloseable { + private val anthropic = AnthropicOkHttpClient.builder() + .apiKey(apiKey) + .build() // Initialize MCP client private val mcp: Client = Client(clientInfo = Implementation(name = "mcp-client-cli", version = "1.0.0")) - private val messageParamsBuilder: MessageCreateParams.Builder = MessageCreateParams.builder() - .model(Model.CLAUDE_4_SONNET_20250514) - .maxTokens(1024) + // Server process reference for cleanup + private var serverProcess: Process? = null // List of tools offered by the server private lateinit var tools: List @@ -44,52 +49,52 @@ class MCPClient : AutoCloseable { // Connect to the server using the path to the server suspend fun connectToServer(serverScriptPath: String) { - try { - // Build the command based on the file extension of the server script - val command = buildList { - when (serverScriptPath.substringAfterLast(".")) { - "js" -> add("node") - "py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3") - "jar" -> addAll(listOf("java", "-jar")) - else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file") - } - add(serverScriptPath) + // Build the command based on the file extension of the server script + val command = buildList { + when (serverScriptPath.substringAfterLast(".")) { + "js" -> add("node") + "py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3") + "jar" -> addAll(listOf("java", "-jar")) + else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file") } + add(serverScriptPath) + } - // Start the server process - val process = ProcessBuilder(command).start() + // Start the server process + val process = withContext(Dispatchers.IO) { + ProcessBuilder(command) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + } + serverProcess = process - // Setup I/O transport using the process streams - val transport = StdioClientTransport( - input = process.inputStream.asSource().buffered(), - output = process.outputStream.asSink().buffered(), - ) + // Setup I/O transport using the process streams + val transport = StdioClientTransport( + input = process.inputStream.asSource().buffered(), + output = process.outputStream.asSink().buffered(), + ) - // Connect the MCP client to the server using the transport - mcp.connect(transport) - - // Request the list of available tools from the server - val toolsResult = mcp.listTools() - tools = toolsResult.tools.map { tool -> - ToolUnion.ofTool( - Tool.builder() - .name(tool.name) - .description(tool.description ?: "") - .inputSchema( - Tool.InputSchema.builder() - .type(JsonValue.from(tool.inputSchema.type)) - .properties(tool.inputSchema.properties?.toJsonValue() ?: EmptyJsonObject.toJsonValue()) - .putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required)) - .build(), - ) - .build(), - ) - } - println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}") - } catch (e: Exception) { - println("Failed to connect to MCP server: $e") - throw e + // Connect the MCP client to the server using the transport + mcp.connect(transport) + + // Request the list of available tools from the server + val toolsResult = mcp.listTools() + tools = toolsResult.tools.map { tool -> + ToolUnion.ofTool( + Tool.builder() + .name(tool.name) + .description(tool.description ?: "") + .inputSchema( + Tool.InputSchema.builder() + .type(JsonValue.from(tool.inputSchema.type)) + .properties(tool.inputSchema.properties?.toJsonValue() ?: EmptyJsonObject.toJsonValue()) + .putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required)) + .build(), + ) + .build(), + ) } + println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}") } // Process a user query and return a string response @@ -104,23 +109,29 @@ class MCPClient : AutoCloseable { // Send the query to the Anthropic model and get the response val response = anthropic.messages().create( - messageParamsBuilder + MessageCreateParams.builder() + .model(MODEL) + .maxTokens(MAX_TOKENS) .messages(messages) .tools(tools) .build(), ) val finalText = mutableListOf() + val toolResults = mutableListOf() + response.content().forEach { content -> when { // Append text outputs from the response - content.isText() -> finalText.add(content.text().getOrNull()?.text() ?: "") + content.isText() -> finalText.add(content.text().get().text()) // If the response indicates a tool use, process it further content.isToolUse() -> { - val toolName = content.toolUse().get().name() + val toolUse = content.toolUse().get() + val toolName = toolUse.name() + val toolUseId = toolUse.id() val toolArgs = - content.toolUse().get()._input().convert(object : TypeReference>() {}) + toolUse._input().convert(object : TypeReference>() {}) // Call the tool with provided arguments val result = mcp.callTool( @@ -129,37 +140,55 @@ class MCPClient : AutoCloseable { ) finalText.add("[Calling tool $toolName with args $toolArgs]") - // Add the tool result message to the conversation - messages.add( - MessageParam.builder() - .role(MessageParam.Role.USER) - .content( - """ - "type": "tool_result", - "tool_name": $toolName, - "result": ${ - result.content.joinToString("\n") { - (it as TextContent).text - } - } - """.trimIndent(), - ) - .build(), - ) + // Build tool_result content block with tool_use_id + val toolResultContent = result.content + .filterIsInstance() + .joinToString("\n") { it.text } - // Retrieve an updated response after tool execution - val aiResponse = anthropic.messages().create( - messageParamsBuilder - .messages(messages) - .build(), - ) + val toolResultBlock = ToolResultBlockParam.builder() + .toolUseId(toolUseId) + .content(toolResultContent) + .apply { if (result.isError == true) isError(true) } + .build() - // Append the updated response to final text - finalText.add(aiResponse.content().first().text().getOrNull()?.text() ?: "") + toolResults.add(ContentBlockParam.ofToolResult(toolResultBlock)) } } } + // If there were tool calls, send tool results back and get final response + if (toolResults.isNotEmpty()) { + // Add the full assistant response (includes tool_use blocks) + messages.add( + MessageParam.builder() + .role(MessageParam.Role.ASSISTANT) + .contentOfBlockParams(response.content().map { it.toParam() }) + .build(), + ) + + // Add user message with tool results + messages.add( + MessageParam.builder() + .role(MessageParam.Role.USER) + .contentOfBlockParams(toolResults) + .build(), + ) + + // Retrieve an updated response after tool execution (without tools) + val aiResponse = anthropic.messages().create( + MessageCreateParams.builder() + .model(MODEL) + .maxTokens(MAX_TOKENS) + .messages(messages) + .build(), + ) + + // Append the updated response to final text + aiResponse.content() + .filter { it.isText() } + .forEach { finalText.add(it.text().get().text()) } + } + return finalText.joinToString("\n", prefix = "", postfix = "") } @@ -171,17 +200,27 @@ class MCPClient : AutoCloseable { while (true) { print("\nQuery: ") val message = readlnOrNull() ?: break - if (message.lowercase() == "quit") break - val response = processQuery(message) - println("\n$response") - yield() + if (message.trim().lowercase() == "quit") break + + try { + val response = processQuery(message) + println("\n$response") + } catch (e: Exception) { + println("\nError: ${e.message}") + } } } override fun close() { runBlocking { mcp.close() - anthropic.close() } + serverProcess?.let { process -> + process.destroy() + if (!process.waitFor(5, TimeUnit.SECONDS)) { + process.destroyForcibly() + } + } + anthropic.close() } } diff --git a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt index 19a3102f5..001a25575 100644 --- a/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt +++ b/samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt @@ -3,13 +3,14 @@ package io.modelcontextprotocol.sample.client import kotlinx.coroutines.runBlocking fun main(args: Array) = runBlocking { - require(args.isNotEmpty()) { - "Usage: java -jar /build/libs/kotlin-mcp-client-0.1.0-all.jar " - } - val serverPath = args.first() - val client = MCPClient() + require(args.isNotEmpty()) { "Usage: java -jar " } + + val apiKey = System.getenv("ANTHROPIC_API_KEY") + require(!apiKey.isNullOrBlank()) { "ANTHROPIC_API_KEY environment variable is not set" } + + val client = MCPClient(apiKey) client.use { - client.connectToServer(serverPath) + client.connectToServer(args.first()) client.chatLoop() } } diff --git a/samples/weather-stdio-server/README.md b/samples/weather-stdio-server/README.md index 4c13040c8..f377d5567 100644 --- a/samples/weather-stdio-server/README.md +++ b/samples/weather-stdio-server/README.md @@ -57,8 +57,8 @@ Add the following to your Claude Desktop configuration: | Name | Description | |----------------|------------------------------------------------------------------------------------------| -| `get_forecast` | Returns weather forecast for a given `latitude` / `longitude` using the weather.gov API. | | `get_alerts` | Returns active weather alerts for a two-letter US `state` code (e.g. `CA`, `NY`). | +| `get_forecast` | Returns weather forecast for a given `latitude` / `longitude` using the weather.gov API. | ## Additional Resources diff --git a/samples/weather-stdio-server/build.gradle.kts b/samples/weather-stdio-server/build.gradle.kts index 0420cc0d8..d6f5fc200 100644 --- a/samples/weather-stdio-server/build.gradle.kts +++ b/samples/weather-stdio-server/build.gradle.kts @@ -14,14 +14,12 @@ version = "0.1.0" dependencies { implementation(dependencies.platform(libs.ktor.bom)) + implementation(libs.mcp.kotlin.server) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) - implementation(libs.mcp.kotlin.server) - implementation(libs.ktor.server.cio) implementation(libs.ktor.client.cio) implementation(libs.slf4j.simple) runtimeOnly(libs.kotlin.logging) - runtimeOnly(libs.kotlinx.collections.immutable) testImplementation(kotlin("test")) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index 6056b9442..dab7f7d29 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -1,22 +1,19 @@ [versions] -collections-immutable = "0.4.0" coroutines = "1.10.2" kotlin = "2.2.21" ktor = "3.2.3" logging = "7.0.13" -mcp-kotlin = "0.8.1" +mcp-kotlin = "0.9.0" shadow = "9.2.2" slf4j = "2.0.17" [libraries] kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "logging" } -kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable-jvm", version.ref = "collections-immutable" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation" } ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json-jvm" } -ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" } mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } diff --git a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt index f08c436d1..1dee706f7 100644 --- a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt +++ b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/McpWeatherServer.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.sample.server import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest import io.ktor.http.ContentType @@ -15,6 +16,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import kotlinx.coroutines.Job import kotlinx.coroutines.runBlocking @@ -32,47 +34,59 @@ import kotlinx.serialization.json.putJsonObject * weather alerts by state and weather forecasts by latitude/longitude. */ fun runMcpServer() { - // Base URL for the Weather API - val baseUrl = "https://api.weather.gov" + createHttpClient().use { httpClient -> + val server = Server( + Implementation( + name = "weather", + version = "1.0.0", + ), + ServerOptions( + capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)), + ), + ) + + server.registerTools(httpClient) - // Create an HTTP client with a default request configuration and JSON content negotiation - val httpClient = HttpClient { - defaultRequest { - url(baseUrl) - headers { - append("Accept", "application/geo+json") - append("User-Agent", "WeatherApiClient/1.0") + val transport = StdioServerTransport( + System.`in`.asInput(), + System.out.asSink().buffered(), + ) + + runBlocking { + val session = server.createSession(transport) + val done = Job() + session.onClose { + done.complete() } - contentType(ContentType.Application.Json) - } - // Install content negotiation plugin for JSON serialization/deserialization - install(ContentNegotiation) { - json( - Json { - ignoreUnknownKeys = true - prettyPrint = true - }, - ) + done.join() } } +} - // Create the MCP Server instance with a basic implementation - val server = Server( - Implementation( - name = "weather", // Tool name is "weather" - version = "1.0.0", // Version of the implementation - ), - ServerOptions( - capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)), - ), - ) +private fun createHttpClient(): HttpClient = HttpClient(CIO) { + defaultRequest { + url("https://api.weather.gov") + headers { + append("Accept", "application/geo+json") + append("User-Agent", "WeatherApiClient/1.0") + } + contentType(ContentType.Application.Json) + } + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + prettyPrint = true + }, + ) + } +} +private fun Server.registerTools(httpClient: HttpClient) { // Register a tool to fetch weather alerts by state - server.addTool( + addTool( name = "get_alerts", - description = """ - Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY) - """.trimIndent(), + description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)", inputSchema = ToolSchema( properties = buildJsonObject { putJsonObject("state") { @@ -82,33 +96,62 @@ fun runMcpServer() { }, required = listOf("state"), ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = true), ) { request -> - val state = request.arguments?.get("state")?.jsonPrimitive?.content ?: return@addTool CallToolResult( - content = listOf(TextContent("The 'state' parameter is required.")), - ) + val state = request.arguments?.get("state")?.jsonPrimitive?.content + ?: return@addTool CallToolResult( + content = listOf(TextContent("The 'state' parameter is required.")), + ) - val alerts = httpClient.getAlerts(state) + if (state.length != 2) { + return@addTool CallToolResult( + content = listOf(TextContent("Invalid state code: '$state'. Must be a two-letter US state code.")), + isError = true, + ) + } - CallToolResult(content = alerts.map { TextContent(it) }) + val stateCode = state.uppercase() + + httpClient.getAlerts(stateCode).fold( + onSuccess = { alerts -> + if (alerts.isEmpty()) { + CallToolResult(content = listOf(TextContent("No active alerts for $stateCode"))) + } else { + val alertsText = "Active alerts for $stateCode:\n\n${alerts.joinToString("\n---\n")}" + CallToolResult(content = listOf(TextContent(alertsText))) + } + }, + onFailure = { e -> + CallToolResult( + content = listOf(TextContent("Failed to retrieve alerts data: ${e.message}")), + isError = true, + ) + }, + ) } // Register a tool to fetch weather forecast by latitude and longitude - server.addTool( + addTool( name = "get_forecast", - description = """ - Get weather forecast for a specific latitude/longitude - """.trimIndent(), + description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.", inputSchema = ToolSchema( properties = buildJsonObject { putJsonObject("latitude") { put("type", "number") + put("description", "Latitude of the location") + put("minimum", -90) + put("maximum", 90) } putJsonObject("longitude") { put("type", "number") + put("description", "Longitude of the location") + put("minimum", -180) + put("maximum", 180) } }, required = listOf("latitude", "longitude"), ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = true), ) { request -> val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull @@ -118,23 +161,26 @@ fun runMcpServer() { ) } - val forecast = httpClient.getForecast(latitude, longitude) - - CallToolResult(content = forecast.map { TextContent(it) }) - } - - // Create a transport using standard IO for server communication - val transport = StdioServerTransport( - System.`in`.asInput(), - System.out.asSink().buffered(), - ) - - runBlocking { - val session = server.createSession(transport) - val done = Job() - session.onClose { - done.complete() - } - done.join() + httpClient.getForecast(latitude, longitude).fold( + onSuccess = { periods -> + if (periods.isEmpty()) { + CallToolResult(content = listOf(TextContent("No forecast periods available"))) + } else { + val forecastText = periods.joinToString("\n---\n") + CallToolResult(content = listOf(TextContent(forecastText))) + } + }, + onFailure = { _ -> + CallToolResult( + content = listOf( + TextContent( + "Failed to retrieve grid point data for coordinates: $latitude, $longitude. " + + "This location may not be supported by the NWS API (only US locations are supported).", + ), + ), + isError = true, + ) + }, + ) } } diff --git a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt index 671186c3e..000ac12ff 100644 --- a/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt +++ b/samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt @@ -4,88 +4,90 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject +import java.util.Locale -// Extension function to fetch forecast information for given latitude and longitude -suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List { - // Build the URI using provided latitude and longitude - val uri = "/points/$latitude,$longitude" - // Request the points data from the API - val points = this.get(uri).body() - - // Request the forecast using the URL provided in the points response - val forecast = this.get(points.properties.forecast).body() +// Extension function to fetch weather alerts for a given state +suspend fun HttpClient.getAlerts(state: String): Result> = runCatching { + val uri = "/alerts/active/area/$state" + val alerts = this.get(uri).body() - // Map each forecast period to a formatted string - return forecast.properties.periods.map { period -> + alerts.features.map { feature -> """ - ${period.name}: - Temperature: ${period.temperature} ${period.temperatureUnit} - Wind: ${period.windSpeed} ${period.windDirection} - Forecast: ${period.detailedForecast} + Event: ${feature.properties.event} + Area: ${feature.properties.areaDesc} + Severity: ${feature.properties.severity} + Status: ${feature.properties.status} + Headline: ${feature.properties.headline} """.trimIndent() } } -// Extension function to fetch weather alerts for a given state -suspend fun HttpClient.getAlerts(state: String): List { - // Build the URI using the given state code - val uri = "/alerts/active/area/$state" - // Request the alerts data from the API - val alerts = this.get(uri).body() +// Extension function to fetch forecast information for given latitude and longitude +suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): Result> = runCatching { + val lat = String.format(Locale.US, "%.4f", latitude) + val lon = String.format(Locale.US, "%.4f", longitude) + val uri = "/points/$lat,$lon" + val points = this.get(uri).body() - // Map each alert feature to a formatted string - return alerts.features.map { feature -> + val forecastUrl = points.properties.forecast + ?: error("No forecast URL available for coordinates: $latitude, $longitude") + val forecast = this.get(forecastUrl).body() + + forecast.properties.periods.map { period -> """ - Event: ${feature.properties.event} - Area: ${feature.properties.areaDesc} - Severity: ${feature.properties.severity} - Description: ${feature.properties.description} - Instruction: ${feature.properties.instruction} + ${period.name}: + Temperature: ${period.temperature}°${period.temperatureUnit} + Wind: ${period.windSpeed} ${period.windDirection} + ${period.shortForecast} """.trimIndent() } } -// Data class representing the points response from the API @Serializable -data class Points( - val properties: Properties -) { - @Serializable - data class Properties(val forecast: String) -} +data class PointsResponse( + val properties: PointsProperties, +) -// Data class representing the forecast response from the API @Serializable -data class Forecast( - val properties: Properties -) { - @Serializable - data class Properties(val periods: List) +data class PointsProperties( + val forecast: String? = null, +) - @Serializable - data class Period( - val number: Int, val name: String, val startTime: String, val endTime: String, - val isDaytime: Boolean, val temperature: Int, val temperatureUnit: String, - val temperatureTrend: String, val probabilityOfPrecipitation: JsonObject, - val windSpeed: String, val windDirection: String, - val shortForecast: String, val detailedForecast: String, - ) -} +@Serializable +data class ForecastResponse( + val properties: ForecastProperties, +) -// Data class representing the alerts response from the API @Serializable -data class Alert( - val features: List -) { - @Serializable - data class Feature( - val properties: Properties - ) +data class ForecastProperties( + val periods: List = emptyList(), +) - @Serializable - data class Properties( - val event: String, val areaDesc: String, val severity: String, - val description: String, val instruction: String?, - ) -} +@Serializable +data class ForecastPeriod( + val name: String? = null, + val temperature: Int? = null, + val temperatureUnit: String? = null, + val windSpeed: String? = null, + val windDirection: String? = null, + val shortForecast: String? = null, +) + +@Serializable +data class AlertsResponse( + val features: List = emptyList(), +) + +@Serializable +data class AlertFeature( + val properties: AlertProperties, +) + +@Serializable +data class AlertProperties( + val event: String? = null, + val areaDesc: String? = null, + val severity: String? = null, + val status: String? = null, + val headline: String? = null, +) diff --git a/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt b/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt index 5934ee262..32fd550e7 100644 --- a/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt +++ b/samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt @@ -13,8 +13,8 @@ fun main(): Unit = runBlocking { val process = ProcessBuilder( "java", "-jar", - "${System.getProperty("user.dir")}/build/libs/weather-stdio-server-0.1.0-all.jar", - ).redirectErrorStream(true) + "${System.getProperty("user.dir")}/samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar", + ).redirectError(ProcessBuilder.Redirect.INHERIT) .start() val transport = StdioClientTransport( @@ -44,7 +44,7 @@ fun main(): Unit = runBlocking { val alertResult = client.callTool( - name = "get_alert", + name = "get_alerts", arguments = mapOf("state" to "TX"), ).content.map { if (it is TextContent) it.text else it.toString() } From cf4e2e40c0ec94d01cc51d65a3ef242f332b18a4 Mon Sep 17 00:00:00 2001 From: Prachi Pandey Date: Fri, 27 Feb 2026 16:56:15 +0530 Subject: [PATCH 021/187] feat: implement Enterprise Managed Authorization (SEP-990) --- kotlin-sdk-client/api/kotlin-sdk-client.api | 198 ++++++ .../sdk/client/auth/AuthServerMetadata.kt | 26 + .../DiscoverAndRequestJwtAuthGrantOptions.kt | 32 + .../kotlin/sdk/client/auth/EnterpriseAuth.kt | 322 +++++++++ .../auth/EnterpriseAuthAssertionContext.kt | 17 + .../client/auth/EnterpriseAuthException.kt | 14 + .../sdk/client/auth/EnterpriseAuthProvider.kt | 249 +++++++ .../auth/EnterpriseAuthProviderOptions.kt | 21 + .../auth/ExchangeJwtBearerGrantOptions.kt | 25 + .../client/auth/JagTokenExchangeResponse.kt | 26 + .../auth/JwtBearerAccessTokenResponse.kt | 42 ++ .../client/auth/RequestJwtAuthGrantOptions.kt | 28 + .../sdk/client/auth/EnterpriseAuthTest.kt | 638 ++++++++++++++++++ 13 files changed, 1638 insertions(+) create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata.kt create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext.kt create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException.kt create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt create mode 100644 kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index da0e752a7..d872c13a2 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -142,3 +142,201 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorCli 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/client/auth/AuthServerMetadata { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata; + public fun equals (Ljava/lang/Object;)Z + public final fun getAuthorizationEndpoint ()Ljava/lang/String; + public final fun getIssuer ()Ljava/lang/String; + public final fun getJwksUri ()Ljava/lang/String; + public final fun getTokenEndpoint ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata; + 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/client/auth/AuthServerMetadata;)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/client/auth/AuthServerMetadata$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAudience ()Ljava/lang/String; + public final fun getClientId ()Ljava/lang/String; + public final fun getClientSecret ()Ljava/lang/String; + public final fun getIdToken ()Ljava/lang/String; + public final fun getIdpTokenEndpoint ()Ljava/lang/String; + public final fun getIdpUrl ()Ljava/lang/String; + public final fun getResource ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth { + public static final field GRANT_TYPE_JWT_BEARER Ljava/lang/String; + public static final field GRANT_TYPE_TOKEN_EXCHANGE Ljava/lang/String; + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth; + public static final field TOKEN_TYPE_ID_JAG Ljava/lang/String; + public static final field TOKEN_TYPE_ID_TOKEN Ljava/lang/String; + public final fun discoverAndRequestJwtAuthorizationGrant (Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun discoverAuthServerMetadata (Ljava/lang/String;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun exchangeJwtBearerGrant (Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun requestJwtAuthorizationGrant (Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun getAuthorizationServerUrl ()Ljava/lang/String; + public final fun getResourceUrl ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException : java/lang/RuntimeException { + public fun (Ljava/lang/String;Ljava/lang/Throwable;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider { + public static final field Plugin Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider$Plugin; + public fun (Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions;Lio/ktor/client/HttpClient;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions;Lio/ktor/client/HttpClient;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun invalidateCache ()V +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider$Config { + public fun ()V + public final fun getAssertionCallback ()Lkotlin/jvm/functions/Function2; + public final fun getAuthHttpClient ()Lio/ktor/client/HttpClient; + public final fun getClientId ()Ljava/lang/String; + public final fun getClientSecret ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; + public final fun setAssertionCallback (Lkotlin/jvm/functions/Function2;)V + public final fun setAuthHttpClient (Lio/ktor/client/HttpClient;)V + public final fun setClientId (Ljava/lang/String;)V + public final fun setClientSecret (Ljava/lang/String;)V + public final fun setScope (Ljava/lang/String;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider$Plugin : io/ktor/client/plugins/HttpClientPlugin { + public fun getKey ()Lio/ktor/util/AttributeKey; + public fun install (Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider;Lio/ktor/client/HttpClient;)V + public synthetic fun install (Ljava/lang/Object;Lio/ktor/client/HttpClient;)V + public fun prepare (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider; + public synthetic fun prepare (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions { + public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAssertionCallback ()Lkotlin/jvm/functions/Function2; + public final fun getClientId ()Ljava/lang/String; + public final fun getClientSecret ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAssertion ()Ljava/lang/String; + public final fun getClientId ()Ljava/lang/String; + public final fun getClientSecret ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; + public final fun getTokenEndpoint ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse; + public fun equals (Ljava/lang/Object;)Z + public final fun getAccessToken ()Ljava/lang/String; + public final fun getExpiresIn ()Ljava/lang/Integer; + public final fun getIssuedTokenType ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; + public final fun getTokenType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse; + 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/client/auth/JagTokenExchangeResponse;)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/client/auth/JagTokenExchangeResponse$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAccessToken ()Ljava/lang/String; + public final fun getExpiresAt ()Lkotlin/time/ComparableTimeMark; + public final fun getExpiresIn ()Ljava/lang/Integer; + public final fun getRefreshToken ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; + public final fun getTokenType ()Ljava/lang/String; + public final fun isExpired ()Z + public final fun setExpiresAt (Lkotlin/time/ComparableTimeMark;)V +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse; + 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/client/auth/JwtBearerAccessTokenResponse;)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/client/auth/JwtBearerAccessTokenResponse$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAudience ()Ljava/lang/String; + public final fun getClientId ()Ljava/lang/String; + public final fun getClientSecret ()Ljava/lang/String; + public final fun getIdToken ()Ljava/lang/String; + public final fun getResource ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; + public final fun getTokenEndpoint ()Ljava/lang/String; +} + diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata.kt new file mode 100644 index 000000000..e14da7d77 --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata.kt @@ -0,0 +1,26 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * OAuth 2.0 Authorization Server Metadata as defined by RFC 8414. + * + * Returned by the `/.well-known/oauth-authorization-server` or + * `/.well-known/openid-configuration` discovery endpoints and used during + * Enterprise Managed Authorization (SEP-990) to locate the token endpoint of the + * enterprise Identity Provider and the MCP authorization server. + * + * @see RFC 8414 + */ +@Serializable +public data class AuthServerMetadata( + /** The authorization server's issuer identifier URI. */ + val issuer: String? = null, + /** The URL of the token endpoint used for token exchange and JWT Bearer grant requests. */ + @SerialName("token_endpoint") val tokenEndpoint: String? = null, + /** The URL of the authorization endpoint (for interactive flows). */ + @SerialName("authorization_endpoint") val authorizationEndpoint: String? = null, + /** The URL of the JSON Web Key Set (JWKS) for public key retrieval. */ + @SerialName("jwks_uri") val jwksUri: String? = null, +) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt new file mode 100644 index 000000000..08cd24e75 --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt @@ -0,0 +1,32 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +/** + * Options for [EnterpriseAuth.discoverAndRequestJwtAuthorizationGrant] — performs Step 1 + * of the Enterprise Managed Authorization (SEP-990) flow, first discovering the IdP's + * token endpoint via RFC 8414 metadata discovery, then requesting the JAG. + * + * If [idpTokenEndpoint] is provided, the discovery step is skipped and the provided + * endpoint is used directly. + * + * @param idpUrl The base URL of the enterprise IdP. Used for RFC 8414 discovery when + * [idpTokenEndpoint] is not set (tries `/.well-known/oauth-authorization-server` and + * then `/.well-known/openid-configuration`). + * @param idToken The OIDC ID token issued by the enterprise IdP. + * @param clientId The OAuth 2.0 client ID registered at the enterprise IdP. + * @param idpTokenEndpoint Optional override for the IdP's token endpoint. When provided, + * RFC 8414 discovery is skipped. + * @param clientSecret The OAuth 2.0 client secret (optional; `null` for public clients). + * @param audience Optional `audience` parameter for the token exchange request. + * @param resource Optional `resource` parameter for the token exchange request. + * @param scope Optional `scope` parameter for the token exchange request. + */ +public class DiscoverAndRequestJwtAuthGrantOptions( + public val idpUrl: String, + public val idToken: String, + public val clientId: String, + public val idpTokenEndpoint: String? = null, + public val clientSecret: String? = null, + public val audience: String? = null, + public val resource: String? = null, + public val scope: String? = null, +) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt new file mode 100644 index 000000000..807fcad47 --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt @@ -0,0 +1,322 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.request.headers +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.http.encodeURLParameter +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource + +private val logger = KotlinLogging.logger {} + +/** + * Layer 2 utility object for the Enterprise Managed Authorization (SEP-990) flow. + * + * Provides standalone `suspend` functions for each discrete step of the two-step enterprise + * auth protocol: + * + * 1. **Step 1 — JAG request:** Exchange an enterprise OIDC ID Token for a JWT + * Authorization Grant (ID-JAG) at the enterprise IdP via RFC 8693 token exchange. + * Use [requestJwtAuthorizationGrant] or [discoverAndRequestJwtAuthorizationGrant]. + * + * 2. **Step 2 — access token exchange:** Exchange the ID-JAG for an OAuth 2.0 access + * token at the MCP authorization server via RFC 7523 JWT Bearer grant. + * Use [exchangeJwtBearerGrant]. + * + * For a higher-level, stateful integration that handles both steps and caches the + * resulting access token, use [EnterpriseAuthProvider] instead. + * + * All functions require a Ktor [HttpClient] to be provided by the caller. They do not + * manage the lifecycle of that client. + * + * @see EnterpriseAuthProvider + * @see RFC 8414 — Authorization Server Metadata + * @see RFC 8693 — Token Exchange + * @see RFC 7523 — JWT Bearer Grant + */ +public object EnterpriseAuth { + + /** + * Token type URI for OIDC ID tokens, used as the `subject_token_type` in the + * RFC 8693 token exchange request. + */ + public const val TOKEN_TYPE_ID_TOKEN: String = "urn:ietf:params:oauth:token-type:id_token" + + /** + * Token type URI for JWT Authorization Grants (ID-JAG), used as the + * `requested_token_type` in the token exchange request and validated as the + * `issued_token_type` in the response. + */ + public const val TOKEN_TYPE_ID_JAG: String = "urn:ietf:params:oauth:token-type:id-jag" + + /** + * Grant type URI for RFC 8693 token exchange requests. + */ + public const val GRANT_TYPE_TOKEN_EXCHANGE: String = "urn:ietf:params:oauth:grant-type:token-exchange" + + /** + * Grant type URI for RFC 7523 JWT Bearer grant requests. + */ + public const val GRANT_TYPE_JWT_BEARER: String = "urn:ietf:params:oauth:grant-type:jwt-bearer" + + private const val WELL_KNOWN_OAUTH: String = "/.well-known/oauth-authorization-server" + private const val WELL_KNOWN_OPENID: String = "/.well-known/openid-configuration" + + // ----------------------------------------------------------------------- + // Authorization server discovery (RFC 8414) + // ----------------------------------------------------------------------- + + /** + * Discovers the OAuth 2.0 authorization server metadata for the given base URL using + * RFC 8414. + * + * First attempts to retrieve metadata from `{url}/.well-known/oauth-authorization-server`. + * If that fails (non-200 response or network error), falls back to + * `{url}/.well-known/openid-configuration`. + * + * @param url The base URL of the authorization or resource server (trailing slash is stripped). + * @param httpClient The HTTP client to use for the discovery request. + * @return The parsed [AuthServerMetadata]. + * @throws EnterpriseAuthException if both endpoints fail. + */ + public suspend fun discoverAuthServerMetadata(url: String, httpClient: HttpClient): AuthServerMetadata { + val baseUrl = url.trimEnd('/') + val oauthUrl = "$baseUrl$WELL_KNOWN_OAUTH" + val openIdUrl = "$baseUrl$WELL_KNOWN_OPENID" + logger.debug { "Discovering authorization server metadata for $baseUrl" } + return try { + fetchAuthServerMetadata(oauthUrl, httpClient) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logger.debug { "OAuth discovery failed ($oauthUrl), falling back to OpenID configuration: ${e.message}" } + fetchAuthServerMetadata(openIdUrl, httpClient) + } + } + + private suspend fun fetchAuthServerMetadata(url: String, httpClient: HttpClient): AuthServerMetadata { + val response = httpClient.get(url) { + headers { append(HttpHeaders.Accept, "application/json") } + } + if (response.status != HttpStatusCode.OK) { + throw EnterpriseAuthException( + "Failed to discover authorization server metadata from $url: HTTP ${response.status.value}", + ) + } + return try { + val body = response.bodyAsText() + val metadata = McpJson.decodeFromString(body) + logger.debug { + "Discovered authorization server metadata from $url: " + + "issuer=${metadata.issuer}, tokenEndpoint=${metadata.tokenEndpoint}" + } + metadata + } catch (e: EnterpriseAuthException) { + throw e + } catch (e: Exception) { + throw EnterpriseAuthException("Failed to parse authorization server metadata from $url", e) + } + } + + // ----------------------------------------------------------------------- + // Step 1 — JAG request (RFC 8693 token exchange) + // ----------------------------------------------------------------------- + + /** + * Requests a JWT Authorization Grant (ID-JAG) by performing an RFC 8693 token exchange + * at the specified token endpoint. + * + * Exchanges the enterprise OIDC ID Token for an ID-JAG that can subsequently be + * presented to the MCP authorization server via [exchangeJwtBearerGrant]. + * + * Validates that the response `issued_token_type` equals [TOKEN_TYPE_ID_JAG] and + * that `token_type` is `N_A` (case-insensitive) per RFC 8693 §2.2.1. + * + * @param options Request parameters including the IdP token endpoint, ID Token, and + * client credentials. + * @param httpClient The HTTP client to use. + * @return The JAG string (the `access_token` value from the exchange response). + * @throws EnterpriseAuthException on HTTP error, unexpected token types, or parse failure. + */ + public suspend fun requestJwtAuthorizationGrant( + options: RequestJwtAuthGrantOptions, + httpClient: HttpClient, + ): String { + val params = buildList { + add("grant_type" to GRANT_TYPE_TOKEN_EXCHANGE) + add("subject_token" to options.idToken) + add("subject_token_type" to TOKEN_TYPE_ID_TOKEN) + add("requested_token_type" to TOKEN_TYPE_ID_JAG) + add("client_id" to options.clientId) + options.clientSecret?.let { add("client_secret" to it) } + options.audience?.let { add("audience" to it) } + options.resource?.let { add("resource" to it) } + options.scope?.let { add("scope" to it) } + } + val body = params.joinToString("&") { (k, v) -> + k.encodeURLParameter().replace("%20", "+") + "=" + v.encodeURLParameter().replace("%20", "+") + } + + logger.debug { "Requesting JAG token exchange at ${options.tokenEndpoint}" } + + val response = httpClient.post(options.tokenEndpoint) { + contentType(ContentType.Application.FormUrlEncoded) + headers { append(HttpHeaders.Accept, "application/json") } + setBody(body) + } + + if (response.status != HttpStatusCode.OK) { + throw EnterpriseAuthException( + "JAG token exchange failed: HTTP ${response.status.value} - ${response.bodyAsText()}", + ) + } + + return try { + val tokenResponse = McpJson.decodeFromString(response.bodyAsText()) + + // Validate per RFC 8693 §2.2.1 + if (!TOKEN_TYPE_ID_JAG.equals(tokenResponse.issuedTokenType, ignoreCase = true)) { + throw EnterpriseAuthException( + "Unexpected issued_token_type in JAG response: " + + "${tokenResponse.issuedTokenType} (expected $TOKEN_TYPE_ID_JAG)", + ) + } + if (!"N_A".equals(tokenResponse.tokenType, ignoreCase = true)) { + throw EnterpriseAuthException( + "Unexpected token_type in JAG response: " + + "${tokenResponse.tokenType} (expected N_A per RFC 8693 §2.2.1)", + ) + } + if (tokenResponse.accessToken.isNullOrBlank()) { + throw EnterpriseAuthException("JAG token exchange response is missing access_token") + } + + logger.debug { "JAG token exchange successful" } + tokenResponse.accessToken + } catch (e: EnterpriseAuthException) { + throw e + } catch (e: Exception) { + throw EnterpriseAuthException("Failed to parse JAG token exchange response", e) + } + } + + /** + * Discovers the enterprise IdP's token endpoint via RFC 8414, then requests a JAG via + * RFC 8693 token exchange. + * + * If [DiscoverAndRequestJwtAuthGrantOptions.idpTokenEndpoint] is set, the discovery + * step is skipped and the provided endpoint is used directly. + * + * @param options Request parameters including the IdP base URL (for discovery), ID Token, + * and client credentials. + * @param httpClient The HTTP client to use. + * @return The JAG string. + * @throws EnterpriseAuthException on discovery or exchange failure. + */ + public suspend fun discoverAndRequestJwtAuthorizationGrant( + options: DiscoverAndRequestJwtAuthGrantOptions, + httpClient: HttpClient, + ): String { + val tokenEndpoint = if (options.idpTokenEndpoint != null) { + options.idpTokenEndpoint + } else { + val metadata = discoverAuthServerMetadata(options.idpUrl, httpClient) + metadata.tokenEndpoint + ?: throw EnterpriseAuthException( + "No token_endpoint in IdP metadata at ${options.idpUrl}. " + + "Ensure the IdP supports RFC 8414.", + ) + } + + return requestJwtAuthorizationGrant( + RequestJwtAuthGrantOptions( + tokenEndpoint = tokenEndpoint, + idToken = options.idToken, + clientId = options.clientId, + clientSecret = options.clientSecret, + audience = options.audience, + resource = options.resource, + scope = options.scope, + ), + httpClient, + ) + } + + // ----------------------------------------------------------------------- + // Step 2 — JWT Bearer grant exchange (RFC 7523) + // ----------------------------------------------------------------------- + + /** + * Exchanges a JWT Authorization Grant (ID-JAG) for an OAuth 2.0 access token at the + * MCP authorization server's token endpoint using RFC 7523. + * + * The returned [JwtBearerAccessTokenResponse] includes the access token and, if the + * server provided an `expires_in` value, an absolute [JwtBearerAccessTokenResponse.expiresAt] + * timestamp computed from [kotlin.time.TimeSource.Monotonic]. + * + * @param options Request parameters including the MCP auth server token endpoint, JAG + * assertion, and client credentials. + * @param httpClient The HTTP client to use. + * @return The [JwtBearerAccessTokenResponse]. + * @throws EnterpriseAuthException on HTTP error, missing `access_token`, or parse failure. + */ + public suspend fun exchangeJwtBearerGrant( + options: ExchangeJwtBearerGrantOptions, + httpClient: HttpClient, + ): JwtBearerAccessTokenResponse { + val params = buildList { + add("grant_type" to GRANT_TYPE_JWT_BEARER) + add("assertion" to options.assertion) + add("client_id" to options.clientId) + options.clientSecret?.let { add("client_secret" to it) } + options.scope?.let { add("scope" to it) } + } + val body = params.joinToString("&") { (k, v) -> + k.encodeURLParameter().replace("%20", "+") + "=" + v.encodeURLParameter().replace("%20", "+") + } + + logger.debug { "Exchanging JWT bearer grant at ${options.tokenEndpoint}" } + + val response = httpClient.post(options.tokenEndpoint) { + contentType(ContentType.Application.FormUrlEncoded) + headers { append(HttpHeaders.Accept, "application/json") } + setBody(body) + } + + if (response.status != HttpStatusCode.OK) { + throw EnterpriseAuthException( + "JWT bearer grant exchange failed: HTTP ${response.status.value} - ${response.bodyAsText()}", + ) + } + + return try { + val tokenResponse = McpJson.decodeFromString(response.bodyAsText()) + + if (tokenResponse.accessToken.isNullOrBlank()) { + throw EnterpriseAuthException("JWT bearer grant exchange response is missing access_token") + } + + // Compute absolute expiry from relative expires_in using a monotonic clock + tokenResponse.expiresIn?.let { expiresIn -> + tokenResponse.expiresAt = TimeSource.Monotonic.markNow() + expiresIn.seconds + } + + logger.debug { "JWT bearer grant exchange successful; expires_in=${tokenResponse.expiresIn}" } + tokenResponse + } catch (e: EnterpriseAuthException) { + throw e + } catch (e: Exception) { + throw EnterpriseAuthException("Failed to parse JWT bearer grant exchange response", e) + } + } +} diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext.kt new file mode 100644 index 000000000..cea50de88 --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext.kt @@ -0,0 +1,17 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +/** + * Context passed to the [EnterpriseAuthProviderOptions.assertionCallback]. + * + * Contains the resource URL of the MCP server and the URL of the authorization server + * discovered for that resource. The callback uses this context to obtain a suitable + * assertion (e.g., an OIDC ID token) from the enterprise IdP. + * + * @param resourceUrl The base URL of the MCP resource being accessed. + * @param authorizationServerUrl The URL of the MCP authorization server discovered for + * the resource (the `issuer` from RFC 8414 metadata, or the resource base URL). + */ +public class EnterpriseAuthAssertionContext( + public val resourceUrl: String, + public val authorizationServerUrl: String, +) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException.kt new file mode 100644 index 000000000..d0d904b5c --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException.kt @@ -0,0 +1,14 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +/** + * Exception thrown when an error occurs during the Enterprise Managed Authorization (SEP-990) flow. + * + * This includes failures during: + * - OAuth 2.0 authorization server metadata discovery (RFC 8414) + * - RFC 8693 token exchange (ID Token → ID-JAG) + * - RFC 7523 JWT Bearer grant exchange (ID-JAG → Access Token) + */ +public class EnterpriseAuthException( + message: String, + cause: Throwable? = null, +) : RuntimeException(message, cause) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt new file mode 100644 index 000000000..49c4c8e0c --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt @@ -0,0 +1,249 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpClientPlugin +import io.ktor.client.plugins.HttpSend +import io.ktor.client.plugins.plugin +import io.ktor.http.DEFAULT_PORT +import io.ktor.http.HttpHeaders +import io.ktor.http.Url +import io.ktor.util.AttributeKey +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.time.Duration.Companion.seconds + +private val logger = KotlinLogging.logger {} + +/** + * Layer 3 implementation of Enterprise Managed Authorization (SEP-990). + * + * Integrates with any Ktor [HttpClient] as an [HttpClientPlugin]. When installed, it + * intercepts every outgoing HTTP request via Ktor's [HttpSend] mechanism and: + * + * 1. Checks an in-memory access token cache. + * 2. If the cache is empty or the token expires within a 30-second buffer, performs the + * full enterprise auth flow: + * a. Discovers the MCP authorization server metadata via RFC 8414. + * b. Invokes the [EnterpriseAuthProviderOptions.assertionCallback] to obtain a JWT + * Authorization Grant (ID-JAG) from the enterprise IdP. + * c. Exchanges the JAG for an OAuth 2.0 access token via RFC 7523. + * d. Caches the resulting token. + * 3. Adds an `Authorization: Bearer {token}` header to the outgoing request. + * + * ## Usage as Ktor plugin (recommended) + * + * ```kotlin + * val httpClient = HttpClient(CIO) { + * install(SSE) + * install(EnterpriseAuthProvider) { + * clientId = "my-mcp-client" + * assertionCallback = { ctx -> + * EnterpriseAuth.requestJwtAuthorizationGrant( + * RequestJwtAuthGrantOptions( + * tokenEndpoint = "https://idp.example.com/token", + * idToken = myIdTokenSupplier(), + * clientId = "my-idp-client", + * audience = ctx.authorizationServerUrl, + * resource = ctx.resourceUrl, + * ), + * authHttpClient, + * ) + * } + * } + * } + * + * val transport = StreamableHttpClientTransport(client = httpClient, url = serverUrl) + * ``` + * + * ## Cache invalidation + * + * ```kotlin + * val provider = httpClient.plugin(EnterpriseAuthProvider) + * provider.invalidateCache() // force full re-fetch on next request + * ``` + * + * ## Direct construction + * + * ```kotlin + * val provider = EnterpriseAuthProvider( + * options = EnterpriseAuthProviderOptions( + * clientId = "my-client", + * assertionCallback = { ctx -> /* obtain JAG */ }, + * ), + * authHttpClient = HttpClient(CIO), + * ) + * ``` + * + * @see EnterpriseAuth + * @see EnterpriseAuthProviderOptions + */ +public class EnterpriseAuthProvider( + private val options: EnterpriseAuthProviderOptions, + private val authHttpClient: HttpClient = HttpClient(), +) { + private val mutex = Mutex() + + private val cachedTokenRef = atomic(null) + + /** + * Invalidates the cached access token, forcing the next request to perform a full + * enterprise auth flow. + * + * Useful after receiving a `401 Unauthorized` response from the MCP server. + */ + public fun invalidateCache() { + logger.debug { "Invalidating cached enterprise auth token" } + cachedTokenRef.value = null + } + + internal suspend fun getAccessToken(requestUrl: Url): String { + // Fast path: read without lock — avoids lock contention on the hot path + val cached = cachedTokenRef.value + if (cached != null && !isExpiredOrNearlyExpired(cached)) { + logger.debug { "Using cached enterprise auth token" } + return requireNotNull(cached.accessToken) { "Cached token has null accessToken" } + } + // Slow path: acquire lock, re-check, then fetch if still stale + return mutex.withLock { + val recheckCached = cachedTokenRef.value + if (recheckCached != null && !isExpiredOrNearlyExpired(recheckCached)) { + logger.debug { "Using cached enterprise auth token (after lock)" } + return@withLock requireNotNull(recheckCached.accessToken) { + "Cached token has null accessToken" + } + } + logger.debug { "Cached enterprise auth token absent or near-expiry; fetching new token" } + val newToken = fetchNewToken(requestUrl) + cachedTokenRef.value = newToken + logger.debug { + "Cached new enterprise auth token; expires_in=${newToken.expiresIn?.let { "${it}s" } ?: "unknown"}" + } + requireNotNull(newToken.accessToken) { "Fetched token has null accessToken" } + } + } + + private fun isExpiredOrNearlyExpired(token: JwtBearerAccessTokenResponse): Boolean { + val expiresAt = token.expiresAt ?: return false + return (expiresAt - EXPIRY_BUFFER).hasPassedNow() + } + + private suspend fun fetchNewToken(requestUrl: Url): JwtBearerAccessTokenResponse { + val resourceBaseUrl = deriveBaseUrl(requestUrl) + logger.debug { "Discovering MCP authorization server for resource $resourceBaseUrl" } + + val metadata = EnterpriseAuth.discoverAuthServerMetadata(resourceBaseUrl, authHttpClient) + val tokenEndpoint = metadata.tokenEndpoint + ?: throw EnterpriseAuthException( + "No token_endpoint in authorization server metadata for $resourceBaseUrl. " + + "Ensure the MCP server supports RFC 8414.", + ) + + // Prefer the issuer URI from metadata; fall back to the resource base URL + val authServerUrl = if (!metadata.issuer.isNullOrBlank()) metadata.issuer else resourceBaseUrl + + val assertionContext = EnterpriseAuthAssertionContext( + resourceUrl = resourceBaseUrl, + authorizationServerUrl = authServerUrl, + ) + logger.debug { + "Invoking assertion callback for resourceUrl=$resourceBaseUrl, authServerUrl=$authServerUrl" + } + + val assertion = options.assertionCallback(assertionContext) + + return EnterpriseAuth.exchangeJwtBearerGrant( + ExchangeJwtBearerGrantOptions( + tokenEndpoint = tokenEndpoint, + assertion = assertion, + clientId = options.clientId, + clientSecret = options.clientSecret, + scope = options.scope, + ), + authHttpClient, + ) + } + + /** + * Companion object that implements [HttpClientPlugin], allowing [EnterpriseAuthProvider] + * to be installed directly on an [HttpClient] via `install(EnterpriseAuthProvider) { ... }`. + */ + public companion object Plugin : HttpClientPlugin { + + /** Proactive refresh buffer: re-fetch token this far before actual expiry. */ + private val EXPIRY_BUFFER = 30.seconds + + override val key: AttributeKey = + AttributeKey("EnterpriseAuthProvider") + + override fun prepare(block: Config.() -> Unit): EnterpriseAuthProvider { + val config = Config().apply(block) + val options = EnterpriseAuthProviderOptions( + clientId = requireNotNull(config.clientId) { "clientId must not be null" }, + assertionCallback = requireNotNull(config.assertionCallback) { + "assertionCallback must not be null" + }, + clientSecret = config.clientSecret, + scope = config.scope, + ) + return EnterpriseAuthProvider( + options = options, + authHttpClient = config.authHttpClient ?: HttpClient(), + ) + } + + override fun install(plugin: EnterpriseAuthProvider, scope: HttpClient) { + scope.plugin(HttpSend).intercept { request -> + val token = plugin.getAccessToken(request.url.build()) + request.headers.append(HttpHeaders.Authorization, "Bearer $token") + execute(request) + } + } + } + + /** + * DSL configuration class for `install(EnterpriseAuthProvider) { ... }`. + */ + public class Config { + /** The OAuth 2.0 client ID registered at the MCP authorization server. Required. */ + public var clientId: String? = null + + /** The OAuth 2.0 client secret. Optional for public clients. */ + public var clientSecret: String? = null + + /** The `scope` parameter for the JWT bearer grant exchange. Optional. */ + public var scope: String? = null + + /** + * Callback that obtains a JWT Authorization Grant (ID-JAG) for the given context. + * + * Receives an [EnterpriseAuthAssertionContext] describing the MCP resource and its + * authorization server, and must return the assertion string (e.g., the result of + * [EnterpriseAuth.requestJwtAuthorizationGrant]). Required. + */ + public var assertionCallback: (suspend (EnterpriseAuthAssertionContext) -> String)? = null + + /** + * HTTP client used for auth discovery and token exchange requests (separate from + * the main MCP client). If not provided, a new [HttpClient] is created using + * engine auto-detection. + */ + public var authHttpClient: HttpClient? = null + } +} + +/** + * Derives the scheme + host + port (if non-default) from the given [Url], stripping + * any path, query, or fragment. This is the URL against which RFC 8414 discovery is performed. + */ +private fun deriveBaseUrl(url: Url): String = buildString { + append(url.protocol.name) + append("://") + append(url.host) + val port = url.port + if (port != url.protocol.defaultPort) { + append(':') + append(port) + } +} diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt new file mode 100644 index 000000000..a59f6b735 --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt @@ -0,0 +1,21 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +/** + * Configuration options for [EnterpriseAuthProvider]. + * + * At minimum, [clientId] and [assertionCallback] are required. + * + * @param clientId The OAuth 2.0 client ID registered at the MCP authorization server. Required. + * @param assertionCallback Callback that obtains a JWT Authorization Grant (ID-JAG) assertion + * for the given [EnterpriseAuthAssertionContext]. Required. The callback receives context + * describing the MCP resource and its authorization server, and must return the assertion + * string (e.g., the result of [EnterpriseAuth.requestJwtAuthorizationGrant]). + * @param clientSecret The OAuth 2.0 client secret. Optional for public clients. + * @param scope The `scope` parameter to request when exchanging the JWT bearer grant. Optional. + */ +public class EnterpriseAuthProviderOptions( + public val clientId: String, + public val assertionCallback: suspend (EnterpriseAuthAssertionContext) -> String, + public val clientSecret: String? = null, + public val scope: String? = null, +) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt new file mode 100644 index 000000000..ac4c0e3db --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt @@ -0,0 +1,25 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +/** + * Options for [EnterpriseAuth.exchangeJwtBearerGrant] — performs Step 2 of the + * Enterprise Managed Authorization (SEP-990) flow. + * + * Posts an RFC 7523 JWT Bearer grant exchange to the MCP authorization server's token + * endpoint, exchanging the JAG (JWT Authorization Grant / ID-JAG) for a standard OAuth + * 2.0 access token that can be used to call the MCP server. + * + * @param tokenEndpoint The full URL of the MCP authorization server's token endpoint. + * @param assertion The JWT Authorization Grant (ID-JAG) obtained from Step 1. + * @param clientId The OAuth 2.0 client ID registered at the MCP authorization server. + * @param clientSecret The OAuth 2.0 client secret (optional; `null` for public clients). + * @param scope Optional `scope` parameter for the token request. + * + * @see RFC 7523 + */ +public class ExchangeJwtBearerGrantOptions( + public val tokenEndpoint: String, + public val assertion: String, + public val clientId: String, + public val clientSecret: String? = null, + public val scope: String? = null, +) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt new file mode 100644 index 000000000..6ad10ae53 --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt @@ -0,0 +1,26 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * RFC 8693 Token Exchange response for the JAG (JWT Authorization Grant) flow. + * + * Returned by the enterprise IdP when exchanging an ID Token for a JWT Authorization + * Grant (ID-JAG) during Enterprise Managed Authorization (SEP-990). + * + * The three key fields validated by [EnterpriseAuth.requestJwtAuthorizationGrant] are: + * - [accessToken] — the issued JAG (despite the name, this is an ID-JAG, not an OAuth access token) + * - [issuedTokenType] — must be `urn:ietf:params:oauth:token-type:id-jag` + * - [tokenType] — must be `N_A` (case-insensitive) per RFC 8693 §2.2.1 + * + * @see RFC 8693 + */ +@Serializable +public data class JagTokenExchangeResponse( + @SerialName("access_token") val accessToken: String? = null, + @SerialName("issued_token_type") val issuedTokenType: String? = null, + @SerialName("token_type") val tokenType: String? = null, + val scope: String? = null, + @SerialName("expires_in") val expiresIn: Int? = null, +) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt new file mode 100644 index 000000000..0e2619d42 --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt @@ -0,0 +1,42 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.time.ComparableTimeMark + +/** + * OAuth 2.0 access token response returned by the MCP authorization server after a + * successful RFC 7523 JWT Bearer grant exchange. + * + * This is the result of Step 2 in the Enterprise Managed Authorization (SEP-990) flow: + * exchanging the JWT Authorization Grant (ID-JAG) for an access token at the MCP + * authorization server. + * + * The [expiresAt] field is **not serialized** — it is computed from [expiresIn] by + * [EnterpriseAuth.exchangeJwtBearerGrant] immediately after deserialization using a + * monotonic time mark. + * + * @see RFC 7523 + */ +@Serializable +public class JwtBearerAccessTokenResponse( + @SerialName("access_token") public val accessToken: String? = null, + @SerialName("token_type") public val tokenType: String? = null, + /** Lifetime of the access token in seconds, as reported by the authorization server. */ + @SerialName("expires_in") public val expiresIn: Int? = null, + public val scope: String? = null, + @SerialName("refresh_token") public val refreshToken: String? = null, +) { + /** + * Absolute expiry time computed from [expiresIn] after deserialization. Not serialized. + * + * Set by [EnterpriseAuth.exchangeJwtBearerGrant] using [kotlin.time.TimeSource.Monotonic]. + */ + public var expiresAt: ComparableTimeMark? = null + + /** + * Returns `true` if this token has passed its [expiresAt] mark, or `false` if + * no expiry information was provided by the server. + */ + public fun isExpired(): Boolean = expiresAt?.hasPassedNow() ?: false +} diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt new file mode 100644 index 000000000..7b65b8504 --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt @@ -0,0 +1,28 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +/** + * Options for [EnterpriseAuth.requestJwtAuthorizationGrant] — performs Step 1 of the + * Enterprise Managed Authorization (SEP-990) flow using a known token endpoint. + * + * Posts an RFC 8693 token exchange request to the enterprise IdP's token endpoint and + * returns the JAG (JWT Authorization Grant / ID-JAG token). + * + * @param tokenEndpoint The full URL of the enterprise IdP's token endpoint. + * @param idToken The OIDC ID token issued by the enterprise IdP. + * @param clientId The OAuth 2.0 client ID registered at the enterprise IdP. + * @param clientSecret The OAuth 2.0 client secret (optional; `null` for public clients). + * @param audience Optional `audience` parameter for the token exchange request. + * @param resource Optional `resource` parameter for the token exchange request. + * @param scope Optional `scope` parameter for the token exchange request. + * + * @see RFC 8693 + */ +public class RequestJwtAuthGrantOptions( + public val tokenEndpoint: String, + public val idToken: String, + public val clientId: String, + public val clientSecret: String? = null, + public val audience: String? = null, + public val resource: String? = null, + public val scope: String? = null, +) diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt new file mode 100644 index 000000000..fea1d54cc --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt @@ -0,0 +1,638 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.MockRequestHandleScope +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.plugin +import io.ktor.client.request.HttpResponseData +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.content.TextContent +import io.ktor.http.headersOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource + +/** + * Tests for [EnterpriseAuth] and [EnterpriseAuthProvider]. + */ +class EnterpriseAuthTest { + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + private val jsonContentTypeHeader = headersOf(HttpHeaders.ContentType, "application/json") + + private fun MockRequestHandleScope.jsonOk(body: String) = + respond(body, HttpStatusCode.OK, jsonContentTypeHeader) + + private fun MockRequestHandleScope.serverError() = + respond("Internal Server Error", HttpStatusCode.InternalServerError) + + /** Builds a mock [HttpClient] that dispatches by URL path. */ + private fun mockHttpClient( + vararg handlers: Pair HttpResponseData>, + ): HttpClient { + val handlerMap = handlers.toMap() + return HttpClient(MockEngine { request -> + val path = request.url.encodedPath + val handler = handlerMap[path] ?: error("Unexpected request to path: $path") + handler() + }) + } + + private fun requestBodyText(request: io.ktor.client.request.HttpRequestData): String = + (request.body as TextContent).text + + // ----------------------------------------------------------------------- + // discoverAuthServerMetadata — success paths + // ----------------------------------------------------------------------- + + @Test + fun `discoverAuthServerMetadata success via oauth well-known`() = runTest { + val httpClient = mockHttpClient( + "/.well-known/oauth-authorization-server" to { + jsonOk( + """{"issuer":"https://auth.example.com","token_endpoint":"https://auth.example.com/token","authorization_endpoint":"https://auth.example.com/authorize"}""", + ) + }, + ) + val metadata = EnterpriseAuth.discoverAuthServerMetadata("https://auth.example.com", httpClient) + metadata.issuer shouldBe "https://auth.example.com" + metadata.tokenEndpoint shouldBe "https://auth.example.com/token" + metadata.authorizationEndpoint shouldBe "https://auth.example.com/authorize" + } + + @Test + fun `discoverAuthServerMetadata falls back to openid-configuration on 404`() = runTest { + val httpClient = mockHttpClient( + "/.well-known/oauth-authorization-server" to { respond("", HttpStatusCode.NotFound) }, + "/.well-known/openid-configuration" to { + jsonOk("""{"issuer":"https://idp.example.com","token_endpoint":"https://idp.example.com/token"}""") + }, + ) + val metadata = EnterpriseAuth.discoverAuthServerMetadata("https://idp.example.com", httpClient) + metadata.tokenEndpoint shouldBe "https://idp.example.com/token" + } + + @Test + fun `discoverAuthServerMetadata falls back to openid-configuration on 500`() = runTest { + val httpClient = mockHttpClient( + "/.well-known/oauth-authorization-server" to { serverError() }, + "/.well-known/openid-configuration" to { + jsonOk("""{"issuer":"https://idp.example.com","token_endpoint":"https://idp.example.com/token"}""") + }, + ) + val metadata = EnterpriseAuth.discoverAuthServerMetadata("https://idp.example.com", httpClient) + metadata.tokenEndpoint shouldBe "https://idp.example.com/token" + } + + @Test + fun `discoverAuthServerMetadata both endpoints fail throws EnterpriseAuthException`() = runTest { + val httpClient = mockHttpClient( + "/.well-known/oauth-authorization-server" to { serverError() }, + "/.well-known/openid-configuration" to { serverError() }, + ) + val ex = shouldThrow { + EnterpriseAuth.discoverAuthServerMetadata("https://auth.example.com", httpClient) + } + ex.message shouldContain "HTTP 500" + } + + @Test + fun `discoverAuthServerMetadata strips trailing slash from url`() = runTest { + val httpClient = mockHttpClient( + "/.well-known/oauth-authorization-server" to { + jsonOk("""{"issuer":"https://auth.example.com","token_endpoint":"https://auth.example.com/token"}""") + }, + ) + val metadata = EnterpriseAuth.discoverAuthServerMetadata("https://auth.example.com/", httpClient) + metadata.issuer shouldBe "https://auth.example.com" + } + + // ----------------------------------------------------------------------- + // requestJwtAuthorizationGrant — success and validation + // ----------------------------------------------------------------------- + + @Test + fun `requestJwtAuthorizationGrant success`() = runTest { + val httpClient = HttpClient(MockEngine { request -> + val body = requestBodyText(request) + body shouldContain "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange" + body shouldContain "subject_token=my-id-token" + body shouldContain "subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token" + body shouldContain "requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid-jag" + body shouldContain "client_id=my-client" + jsonOk( + """{"access_token":"my-jag-token","issued_token_type":"urn:ietf:params:oauth:token-type:id-jag","token_type":"N_A"}""", + ) + }) + + val jag = EnterpriseAuth.requestJwtAuthorizationGrant( + RequestJwtAuthGrantOptions( + tokenEndpoint = "https://idp.example.com/token", + idToken = "my-id-token", + clientId = "my-client", + ), + httpClient, + ) + jag shouldBe "my-jag-token" + } + + @Test + fun `requestJwtAuthorizationGrant includes optional params in body`() = runTest { + val httpClient = HttpClient(MockEngine { request -> + val body = requestBodyText(request) + body shouldContain "client_secret=s3cr3t" + body shouldContain "audience=my-audience" + body shouldContain "resource=https%3A%2F%2Fmcp.example.com" + body shouldContain "scope=read+write" + jsonOk( + """{"access_token":"jag","issued_token_type":"urn:ietf:params:oauth:token-type:id-jag","token_type":"N_A"}""", + ) + }) + + EnterpriseAuth.requestJwtAuthorizationGrant( + RequestJwtAuthGrantOptions( + tokenEndpoint = "https://idp.example.com/token", + idToken = "id-token", + clientId = "client", + clientSecret = "s3cr3t", + audience = "my-audience", + resource = "https://mcp.example.com", + scope = "read write", + ), + httpClient, + ) shouldBe "jag" + } + + @Test + fun `requestJwtAuthorizationGrant wrong issued_token_type throws`() = runTest { + val httpClient = mockHttpClient( + "/token" to { + jsonOk("""{"access_token":"jag","issued_token_type":"urn:wrong:type","token_type":"N_A"}""") + }, + ) + val ex = shouldThrow { + EnterpriseAuth.requestJwtAuthorizationGrant( + RequestJwtAuthGrantOptions("https://idp.example.com/token", "id", "client"), + httpClient, + ) + } + ex.message shouldContain "issued_token_type" + ex.message shouldContain "urn:wrong:type" + } + + @Test + fun `requestJwtAuthorizationGrant wrong token_type throws`() = runTest { + val httpClient = mockHttpClient( + "/token" to { + jsonOk( + """{"access_token":"jag","issued_token_type":"urn:ietf:params:oauth:token-type:id-jag","token_type":"Bearer"}""", + ) + }, + ) + val ex = shouldThrow { + EnterpriseAuth.requestJwtAuthorizationGrant( + RequestJwtAuthGrantOptions("https://idp.example.com/token", "id", "client"), + httpClient, + ) + } + ex.message shouldContain "token_type" + ex.message shouldContain "Bearer" + } + + @Test + fun `requestJwtAuthorizationGrant missing access_token throws`() = runTest { + val httpClient = mockHttpClient( + "/token" to { + jsonOk("""{"issued_token_type":"urn:ietf:params:oauth:token-type:id-jag","token_type":"N_A"}""") + }, + ) + val ex = shouldThrow { + EnterpriseAuth.requestJwtAuthorizationGrant( + RequestJwtAuthGrantOptions("https://idp.example.com/token", "id", "client"), + httpClient, + ) + } + ex.message shouldContain "access_token" + } + + @Test + fun `requestJwtAuthorizationGrant HTTP error throws`() = runTest { + val httpClient = mockHttpClient("/token" to { serverError() }) + val ex = shouldThrow { + EnterpriseAuth.requestJwtAuthorizationGrant( + RequestJwtAuthGrantOptions("https://idp.example.com/token", "id", "client"), + httpClient, + ) + } + ex.message shouldContain "HTTP 500" + } + + // ----------------------------------------------------------------------- + // discoverAndRequestJwtAuthorizationGrant + // ----------------------------------------------------------------------- + + @Test + fun `discoverAndRequestJwtAuthorizationGrant full discovery flow`() = runTest { + val httpClient = mockHttpClient( + "/.well-known/oauth-authorization-server" to { + jsonOk("""{"token_endpoint":"https://idp.example.com/token"}""") + }, + "/token" to { + jsonOk( + """{"access_token":"the-jag","issued_token_type":"urn:ietf:params:oauth:token-type:id-jag","token_type":"N_A"}""", + ) + }, + ) + val jag = EnterpriseAuth.discoverAndRequestJwtAuthorizationGrant( + DiscoverAndRequestJwtAuthGrantOptions( + idpUrl = "https://idp.example.com", + idToken = "my-id-token", + clientId = "my-client", + ), + httpClient, + ) + jag shouldBe "the-jag" + } + + @Test + fun `discoverAndRequestJwtAuthorizationGrant skips discovery when idpTokenEndpoint provided`() = runTest { + var discoveryCallCount = 0 + val httpClient = HttpClient(MockEngine { request -> + if (request.url.encodedPath.contains(".well-known")) discoveryCallCount++ + jsonOk( + """{"access_token":"the-jag","issued_token_type":"urn:ietf:params:oauth:token-type:id-jag","token_type":"N_A"}""", + ) + }) + + EnterpriseAuth.discoverAndRequestJwtAuthorizationGrant( + DiscoverAndRequestJwtAuthGrantOptions( + idpUrl = "https://idp.example.com", + idToken = "my-id-token", + clientId = "my-client", + idpTokenEndpoint = "https://idp.example.com/custom-token", + ), + httpClient, + ) shouldBe "the-jag" + + discoveryCallCount shouldBe 0 + } + + @Test + fun `discoverAndRequestJwtAuthorizationGrant no token_endpoint in metadata throws`() = runTest { + val httpClient = mockHttpClient( + "/.well-known/oauth-authorization-server" to { + jsonOk("""{"issuer":"https://idp.example.com"}""") // no token_endpoint + }, + ) + val ex = shouldThrow { + EnterpriseAuth.discoverAndRequestJwtAuthorizationGrant( + DiscoverAndRequestJwtAuthGrantOptions( + idpUrl = "https://idp.example.com", + idToken = "id", + clientId = "client", + ), + httpClient, + ) + } + ex.message shouldContain "token_endpoint" + } + + // ----------------------------------------------------------------------- + // exchangeJwtBearerGrant + // ----------------------------------------------------------------------- + + @Test + fun `exchangeJwtBearerGrant success with expiresAt computed`() = runTest { + val httpClient = HttpClient(MockEngine { request -> + val body = requestBodyText(request) + body shouldContain "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer" + body shouldContain "assertion=my-jag" + body shouldContain "client_id=my-client" + jsonOk( + """{"access_token":"access-token-123","token_type":"Bearer","expires_in":3600}""", + ) + }) + + val markBefore = TimeSource.Monotonic.markNow() + val tokenResponse = EnterpriseAuth.exchangeJwtBearerGrant( + ExchangeJwtBearerGrantOptions( + tokenEndpoint = "https://auth.example.com/token", + assertion = "my-jag", + clientId = "my-client", + ), + httpClient, + ) + + tokenResponse.accessToken shouldBe "access-token-123" + tokenResponse.tokenType shouldBe "Bearer" + tokenResponse.expiresIn shouldBe 3600 + tokenResponse.expiresAt shouldNotBe null + tokenResponse.isExpired().shouldBeFalse() + + // expiresAt should be approximately 1 hour (3600s) from now; at minimum 59 minutes away + val fiftyNineMinutesFromNow = markBefore + 59.minutes + tokenResponse.expiresAt!!.minus(fiftyNineMinutesFromNow).isPositive().shouldBeTrue() + } + + @Test + fun `exchangeJwtBearerGrant missing access_token throws`() = runTest { + val httpClient = mockHttpClient( + "/token" to { jsonOk("""{"token_type":"Bearer","expires_in":3600}""") }, + ) + val ex = shouldThrow { + EnterpriseAuth.exchangeJwtBearerGrant( + ExchangeJwtBearerGrantOptions("https://auth.example.com/token", "jag", "client"), + httpClient, + ) + } + ex.message shouldContain "access_token" + } + + @Test + fun `exchangeJwtBearerGrant HTTP error throws`() = runTest { + val httpClient = mockHttpClient("/token" to { serverError() }) + val ex = shouldThrow { + EnterpriseAuth.exchangeJwtBearerGrant( + ExchangeJwtBearerGrantOptions("https://auth.example.com/token", "jag", "client"), + httpClient, + ) + } + ex.message shouldContain "HTTP 500" + } + + // ----------------------------------------------------------------------- + // JwtBearerAccessTokenResponse.isExpired + // ----------------------------------------------------------------------- + + @Test + fun `isExpired returns false when expiresAt is null`() { + val response = JwtBearerAccessTokenResponse(accessToken = "token") + response.isExpired().shouldBeFalse() + } + + @Test + fun `isExpired returns false when expiresAt is in the future`() { + val response = JwtBearerAccessTokenResponse(accessToken = "token") + response.expiresAt = TimeSource.Monotonic.markNow() + 3600.seconds + response.isExpired().shouldBeFalse() + } + + @Test + fun `isExpired returns true when expiresAt has passed`() { + val response = JwtBearerAccessTokenResponse(accessToken = "token") + response.expiresAt = TimeSource.Monotonic.markNow() - 1.seconds + response.isExpired().shouldBeTrue() + } + + // ----------------------------------------------------------------------- + // EnterpriseAuthProvider — plugin integration + // ----------------------------------------------------------------------- + + /** Builds a standard mock auth engine (discovery + token exchange) for provider tests. */ + private fun buildMockAuthEngine( + tokenEndpointCallTracker: MutableList = mutableListOf(), + accessTokenValue: String = "mcp-access-token", + ) = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"issuer":"https://auth.example.com","token_endpoint":"https://auth.example.com/token"}""") + "/token" -> { + tokenEndpointCallTracker.add(tokenEndpointCallTracker.size + 1) + jsonOk("""{"access_token":"$accessTokenValue","token_type":"Bearer","expires_in":3600}""") + } + else -> error("Unexpected auth request: ${request.url}") + } + } + + @Test + fun `provider injects Authorization Bearer header into request`() = runTest { + val capturedAuth = mutableListOf() + val mcpClient = HttpClient(MockEngine { request -> + capturedAuth += request.headers[HttpHeaders.Authorization] ?: "" + respond("OK", HttpStatusCode.OK) + }) { + install(EnterpriseAuthProvider) { + clientId = "test-client" + assertionCallback = { _ -> "test-jag" } + authHttpClient = HttpClient(buildMockAuthEngine()) + } + } + + mcpClient.get("http://mcp.example.com/messages") + + capturedAuth shouldBe listOf("Bearer mcp-access-token") + } + + @Test + fun `provider caches token across multiple requests`() = runTest { + val tokenEndpointCalls = mutableListOf() + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(buildMockAuthEngine(tokenEndpointCalls)) + } + } + + mcpClient.get("http://mcp.example.com/messages") + mcpClient.get("http://mcp.example.com/messages") + mcpClient.get("http://mcp.example.com/messages") + + tokenEndpointCalls.size shouldBe 1 + } + + @Test + fun `invalidateCache forces re-fetch on next request`() = runTest { + var tokenCounter = 0 + val capturedAuth = mutableListOf() + + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"token_endpoint":"https://auth.example.com/token"}""") + "/token" -> { + tokenCounter++ + jsonOk("""{"access_token":"token-$tokenCounter","token_type":"Bearer","expires_in":3600}""") + } + else -> error("Unexpected: ${request.url}") + } + } + + val mcpClient = HttpClient(MockEngine { request -> + capturedAuth += request.headers[HttpHeaders.Authorization] ?: "" + respond("OK", HttpStatusCode.OK) + }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(authEngine) + } + } + + mcpClient.get("http://mcp.example.com/messages") // fetches token-1 + + val provider = mcpClient.plugin(EnterpriseAuthProvider) + provider.invalidateCache() + + mcpClient.get("http://mcp.example.com/messages") // fetches token-2 + + tokenCounter shouldBe 2 + capturedAuth[0] shouldBe "Bearer token-1" + capturedAuth[1] shouldBe "Bearer token-2" + } + + @Test + fun `provider discovery failure propagates as EnterpriseAuthException`() = runTest { + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> serverError() + "/.well-known/openid-configuration" -> serverError() + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(authEngine) + } + } + + shouldThrow { + mcpClient.get("http://mcp.example.com/messages") + } + } + + @Test + fun `provider assertion callback error propagates`() = runTest { + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"token_endpoint":"https://auth.example.com/token"}""") + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> throw RuntimeException("callback failed") } + authHttpClient = HttpClient(authEngine) + } + } + + shouldThrow { + mcpClient.get("http://mcp.example.com/messages") + }.message shouldBe "callback failed" + } + + @Test + fun `provider passes correct resourceUrl and authorizationServerUrl to callback`() = runTest { + var capturedContext: EnterpriseAuthAssertionContext? = null + + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"issuer":"https://auth.example.com","token_endpoint":"https://auth.example.com/token"}""") + "/token" -> + jsonOk("""{"access_token":"token","token_type":"Bearer","expires_in":3600}""") + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { ctx -> + capturedContext = ctx + "jag" + } + authHttpClient = HttpClient(authEngine) + } + } + + mcpClient.get("http://mcp.example.com/messages") + + capturedContext shouldNotBe null + capturedContext!!.resourceUrl shouldBe "http://mcp.example.com" + capturedContext!!.authorizationServerUrl shouldBe "https://auth.example.com" + } + + // ----------------------------------------------------------------------- + // EnterpriseAuthProvider.prepare — options validation + // ----------------------------------------------------------------------- + + @Test + fun `prepare throws when clientId is null`() { + shouldThrow { + EnterpriseAuthProvider.prepare { + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) + // clientId not set + } + }.message shouldContain "clientId" + } + + @Test + fun `prepare throws when assertionCallback is null`() { + shouldThrow { + EnterpriseAuthProvider.prepare { + clientId = "my-client" + authHttpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) + // assertionCallback not set + } + }.message shouldContain "assertionCallback" + } + + // ----------------------------------------------------------------------- + // Full end-to-end via plugin — header propagated correctly + // ----------------------------------------------------------------------- + + @Test + fun `full flow via plugin - Authorization header set and token is reused`() = runTest { + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"token_endpoint":"https://idp.example.com/token"}""") + "/token" -> + jsonOk("""{"access_token":"final-access-token","token_type":"Bearer","expires_in":3600}""") + else -> error("Unexpected: ${request.url}") + } + } + + val capturedAuthHeaders = mutableListOf() + val mcpEngine = MockEngine { request -> + capturedAuthHeaders += request.headers[HttpHeaders.Authorization] ?: "" + respond("OK", HttpStatusCode.OK) + } + + val mcpClient = HttpClient(mcpEngine) { + install(EnterpriseAuthProvider) { + clientId = "mcp-client" + assertionCallback = { _ -> "enterprise-jag" } + authHttpClient = HttpClient(authEngine) + } + } + + val response1 = mcpClient.get("http://mcp.example.com/sse") + val response2 = mcpClient.get("http://mcp.example.com/sse") + + response1.bodyAsText() shouldBe "OK" + response2.bodyAsText() shouldBe "OK" + capturedAuthHeaders.all { it == "Bearer final-access-token" }.shouldBeTrue() + } +} + From 8c26488ac66e308e9d2300668f27a487f1c6c0d9 Mon Sep 17 00:00:00 2001 From: Prachi Pandey Date: Tue, 17 Mar 2026 10:57:06 +0530 Subject: [PATCH 022/187] feat(auth): enterprise auth provider changes --- kotlin-sdk-client/api/kotlin-sdk-client.api | 7 +- .../kotlin/sdk/client/auth/EnterpriseAuth.kt | 68 ++-- .../sdk/client/auth/EnterpriseAuthProvider.kt | 16 +- .../auth/EnterpriseAuthProviderOptions.kt | 6 + .../auth/ExchangeJwtBearerGrantOptions.kt | 4 + .../client/auth/JagTokenExchangeResponse.kt | 6 +- .../client/auth/EnterpriseAuthProviderTest.kt | 365 ++++++++++++++++++ .../sdk/client/auth/EnterpriseAuthTest.kt | 268 +------------ 8 files changed, 452 insertions(+), 288 deletions(-) create mode 100644 kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index d872c13a2..02058b4ed 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -226,11 +226,13 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth public final fun getAuthHttpClient ()Lio/ktor/client/HttpClient; public final fun getClientId ()Ljava/lang/String; public final fun getClientSecret ()Ljava/lang/String; + public final fun getExpiryBuffer-UwyO8pc ()J public final fun getScope ()Ljava/lang/String; public final fun setAssertionCallback (Lkotlin/jvm/functions/Function2;)V public final fun setAuthHttpClient (Lio/ktor/client/HttpClient;)V public final fun setClientId (Ljava/lang/String;)V public final fun setClientSecret (Ljava/lang/String;)V + public final fun setExpiryBuffer-LRDsOJo (J)V public final fun setScope (Ljava/lang/String;)V } @@ -243,11 +245,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth } public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions { - public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;JLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getAssertionCallback ()Lkotlin/jvm/functions/Function2; public final fun getClientId ()Ljava/lang/String; public final fun getClientSecret ()Ljava/lang/String; + public final fun getExpiryBuffer-UwyO8pc ()J public final fun getScope ()Ljava/lang/String; } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt index 807fcad47..83c43bbb7 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt @@ -12,6 +12,7 @@ import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.contentType import io.ktor.http.encodeURLParameter +import io.ktor.util.encodeBase64 import io.modelcontextprotocol.kotlin.sdk.types.McpJson import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration.Companion.seconds @@ -72,6 +73,10 @@ public object EnterpriseAuth { private const val WELL_KNOWN_OAUTH: String = "/.well-known/oauth-authorization-server" private const val WELL_KNOWN_OPENID: String = "/.well-known/openid-configuration" + /** URL-encodes [key] and [value] and joins them with `=`, using `+` for spaces. */ + private fun encodeParam(key: String, value: String): String = + key.encodeURLParameter().replace("%20", "+") + "=" + value.encodeURLParameter().replace("%20", "+") + // ----------------------------------------------------------------------- // Authorization server discovery (RFC 8414) // ----------------------------------------------------------------------- @@ -139,8 +144,7 @@ public object EnterpriseAuth { * Exchanges the enterprise OIDC ID Token for an ID-JAG that can subsequently be * presented to the MCP authorization server via [exchangeJwtBearerGrant]. * - * Validates that the response `issued_token_type` equals [TOKEN_TYPE_ID_JAG] and - * that `token_type` is `N_A` (case-insensitive) per RFC 8693 §2.2.1. + * Validates that the response `issued_token_type` equals [TOKEN_TYPE_ID_JAG]. * * @param options Request parameters including the IdP token endpoint, ID Token, and * client credentials. @@ -163,9 +167,7 @@ public object EnterpriseAuth { options.resource?.let { add("resource" to it) } options.scope?.let { add("scope" to it) } } - val body = params.joinToString("&") { (k, v) -> - k.encodeURLParameter().replace("%20", "+") + "=" + v.encodeURLParameter().replace("%20", "+") - } + val body = params.joinToString("&") { (k, v) -> encodeParam(k, v) } logger.debug { "Requesting JAG token exchange at ${options.tokenEndpoint}" } @@ -184,25 +186,10 @@ public object EnterpriseAuth { return try { val tokenResponse = McpJson.decodeFromString(response.bodyAsText()) - // Validate per RFC 8693 §2.2.1 - if (!TOKEN_TYPE_ID_JAG.equals(tokenResponse.issuedTokenType, ignoreCase = true)) { - throw EnterpriseAuthException( - "Unexpected issued_token_type in JAG response: " + - "${tokenResponse.issuedTokenType} (expected $TOKEN_TYPE_ID_JAG)", - ) - } - if (!"N_A".equals(tokenResponse.tokenType, ignoreCase = true)) { - throw EnterpriseAuthException( - "Unexpected token_type in JAG response: " + - "${tokenResponse.tokenType} (expected N_A per RFC 8693 §2.2.1)", - ) - } - if (tokenResponse.accessToken.isNullOrBlank()) { - throw EnterpriseAuthException("JAG token exchange response is missing access_token") - } + validateJAGTokenExchangeResponse(tokenResponse) logger.debug { "JAG token exchange successful" } - tokenResponse.accessToken + tokenResponse.accessToken!! } catch (e: EnterpriseAuthException) { throw e } catch (e: Exception) { @@ -210,6 +197,20 @@ public object EnterpriseAuth { } } + private fun validateJAGTokenExchangeResponse(tokenResponse: JagTokenExchangeResponse) { + if (!TOKEN_TYPE_ID_JAG.equals(tokenResponse.issuedTokenType, ignoreCase = true)) { + throw EnterpriseAuthException( + "Unexpected issued_token_type in JAG response: " + + "${tokenResponse.issuedTokenType} (expected $TOKEN_TYPE_ID_JAG)", + ) + } + // token_type is informational per RFC 8693 §2.2.1; not strictly validated + // because some conformant IdPs omit or vary the field. + if (tokenResponse.accessToken.isNullOrBlank()) { + throw EnterpriseAuthException("JAG token exchange response is missing access_token") + } + } + /** * Discovers the enterprise IdP's token endpoint via RFC 8414, then requests a JAG via * RFC 8693 token exchange. @@ -228,6 +229,8 @@ public object EnterpriseAuth { httpClient: HttpClient, ): String { val tokenEndpoint = if (options.idpTokenEndpoint != null) { + // Caller has already performed RFC 8414 discovery (or knows the endpoint ahead of time); + // skip the discovery round-trip. options.idpTokenEndpoint } else { val metadata = discoverAuthServerMetadata(options.idpUrl, httpClient) @@ -277,19 +280,23 @@ public object EnterpriseAuth { val params = buildList { add("grant_type" to GRANT_TYPE_JWT_BEARER) add("assertion" to options.assertion) - add("client_id" to options.clientId) - options.clientSecret?.let { add("client_secret" to it) } options.scope?.let { add("scope" to it) } } - val body = params.joinToString("&") { (k, v) -> - k.encodeURLParameter().replace("%20", "+") + "=" + v.encodeURLParameter().replace("%20", "+") - } + val body = params.joinToString("&") { (k, v) -> encodeParam(k, v) } + + // Client credentials are sent using client_secret_basic (RFC 6749 §2.3.1): + // clientId and clientSecret are Base64-encoded and sent in the Authorization: Basic header. + val credentials = "${options.clientId}:${options.clientSecret ?: ""}" + val basicAuth = credentials.encodeToByteArray().encodeBase64() logger.debug { "Exchanging JWT bearer grant at ${options.tokenEndpoint}" } val response = httpClient.post(options.tokenEndpoint) { contentType(ContentType.Application.FormUrlEncoded) - headers { append(HttpHeaders.Accept, "application/json") } + headers { + append(HttpHeaders.Accept, "application/json") + append(HttpHeaders.Authorization, "Basic $basicAuth") + } setBody(body) } @@ -306,6 +313,11 @@ public object EnterpriseAuth { throw EnterpriseAuthException("JWT bearer grant exchange response is missing access_token") } + // RFC 7523 is a stateless grant — no refresh token is expected or used. + // If the AS returns one, we intentionally ignore it: using it would bypass + // re-validation of the user's identity with the IdP and undermine + // session / revocation policies. + // Compute absolute expiry from relative expires_in using a monotonic clock tokenResponse.expiresIn?.let { expiresIn -> tokenResponse.expiresAt = TimeSource.Monotonic.markNow() + expiresIn.seconds diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt index 49c4c8e0c..f05ac70f4 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt @@ -5,13 +5,13 @@ import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpClientPlugin import io.ktor.client.plugins.HttpSend import io.ktor.client.plugins.plugin -import io.ktor.http.DEFAULT_PORT import io.ktor.http.HttpHeaders import io.ktor.http.Url import io.ktor.util.AttributeKey import kotlinx.atomicfu.atomic import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds private val logger = KotlinLogging.logger {} @@ -45,6 +45,7 @@ private val logger = KotlinLogging.logger {} * tokenEndpoint = "https://idp.example.com/token", * idToken = myIdTokenSupplier(), * clientId = "my-idp-client", + * clientSecret = "idp-client-secret", * audience = ctx.authorizationServerUrl, * resource = ctx.resourceUrl, * ), @@ -126,7 +127,7 @@ public class EnterpriseAuthProvider( private fun isExpiredOrNearlyExpired(token: JwtBearerAccessTokenResponse): Boolean { val expiresAt = token.expiresAt ?: return false - return (expiresAt - EXPIRY_BUFFER).hasPassedNow() + return (expiresAt - options.expiryBuffer).hasPassedNow() } private suspend fun fetchNewToken(requestUrl: Url): JwtBearerAccessTokenResponse { @@ -152,6 +153,7 @@ public class EnterpriseAuthProvider( } val assertion = options.assertionCallback(assertionContext) + require(assertion.isNotBlank()) { "assertionCallback returned a blank string" } return EnterpriseAuth.exchangeJwtBearerGrant( ExchangeJwtBearerGrantOptions( @@ -171,9 +173,6 @@ public class EnterpriseAuthProvider( */ public companion object Plugin : HttpClientPlugin { - /** Proactive refresh buffer: re-fetch token this far before actual expiry. */ - private val EXPIRY_BUFFER = 30.seconds - override val key: AttributeKey = AttributeKey("EnterpriseAuthProvider") @@ -186,6 +185,7 @@ public class EnterpriseAuthProvider( }, clientSecret = config.clientSecret, scope = config.scope, + expiryBuffer = config.expiryBuffer, ) return EnterpriseAuthProvider( options = options, @@ -215,6 +215,12 @@ public class EnterpriseAuthProvider( /** The `scope` parameter for the JWT bearer grant exchange. Optional. */ public var scope: String? = null + /** + * How far before the token's actual expiry to proactively refresh it. + * Defaults to 30 seconds to account for clock skew and network latency. + */ + public var expiryBuffer: Duration = 30.seconds + /** * Callback that obtains a JWT Authorization Grant (ID-JAG) for the given context. * diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt index a59f6b735..d6a52e716 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt @@ -1,5 +1,8 @@ package io.modelcontextprotocol.kotlin.sdk.client.auth +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + /** * Configuration options for [EnterpriseAuthProvider]. * @@ -12,10 +15,13 @@ package io.modelcontextprotocol.kotlin.sdk.client.auth * string (e.g., the result of [EnterpriseAuth.requestJwtAuthorizationGrant]). * @param clientSecret The OAuth 2.0 client secret. Optional for public clients. * @param scope The `scope` parameter to request when exchanging the JWT bearer grant. Optional. + * @param expiryBuffer How far before the token's actual expiry to proactively refresh it. + * Defaults to 30 seconds to account for clock skew and network latency. */ public class EnterpriseAuthProviderOptions( public val clientId: String, public val assertionCallback: suspend (EnterpriseAuthAssertionContext) -> String, public val clientSecret: String? = null, public val scope: String? = null, + public val expiryBuffer: Duration = 30.seconds, ) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt index ac4c0e3db..30ebeda9d 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt @@ -8,6 +8,10 @@ package io.modelcontextprotocol.kotlin.sdk.client.auth * endpoint, exchanging the JAG (JWT Authorization Grant / ID-JAG) for a standard OAuth * 2.0 access token that can be used to call the MCP server. * + * Client credentials are sent using `client_secret_basic` (RFC 6749 §2.3.1): the + * [clientId] and [clientSecret] are Base64-encoded and sent in an `Authorization: Basic` + * header. This matches SEP-990 conformance requirements. + * * @param tokenEndpoint The full URL of the MCP authorization server's token endpoint. * @param assertion The JWT Authorization Grant (ID-JAG) obtained from Step 1. * @param clientId The OAuth 2.0 client ID registered at the MCP authorization server. diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt index 6ad10ae53..cb9cff7f2 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt @@ -9,10 +9,12 @@ import kotlinx.serialization.Serializable * Returned by the enterprise IdP when exchanging an ID Token for a JWT Authorization * Grant (ID-JAG) during Enterprise Managed Authorization (SEP-990). * - * The three key fields validated by [EnterpriseAuth.requestJwtAuthorizationGrant] are: + * The key fields are: * - [accessToken] — the issued JAG (despite the name, this is an ID-JAG, not an OAuth access token) * - [issuedTokenType] — must be `urn:ietf:params:oauth:token-type:id-jag` - * - [tokenType] — must be `N_A` (case-insensitive) per RFC 8693 §2.2.1 + * - [tokenType] — informational; per RFC 8693 §2.2.1 it SHOULD be `N_A` when the issued + * token is not an access token, but this is not strictly enforced as some conformant + * IdPs may omit or vary the field * * @see RFC 8693 */ diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt new file mode 100644 index 000000000..8f33b3f52 --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt @@ -0,0 +1,365 @@ +package io.modelcontextprotocol.kotlin.sdk.client.auth + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.MockRequestHandleScope +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.plugin +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +/** + * Tests for [EnterpriseAuthProvider] — the Ktor plugin that handles the full enterprise + * auth flow, caching, and header injection. + */ +class EnterpriseAuthProviderTest { + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + private val jsonContentTypeHeader = headersOf(HttpHeaders.ContentType, "application/json") + + private fun MockRequestHandleScope.jsonOk(body: String) = + respond(body, HttpStatusCode.OK, jsonContentTypeHeader) + + private fun MockRequestHandleScope.serverError() = + respond("Internal Server Error", HttpStatusCode.InternalServerError) + + /** Builds a standard mock auth engine (discovery + token exchange) for provider tests. */ + private fun buildMockAuthEngine( + tokenEndpointCallTracker: MutableList = mutableListOf(), + accessTokenValue: String = "mcp-access-token", + ) = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"issuer":"https://auth.example.com","token_endpoint":"https://auth.example.com/token"}""") + "/token" -> { + tokenEndpointCallTracker.add(tokenEndpointCallTracker.size + 1) + jsonOk("""{"access_token":"$accessTokenValue","token_type":"Bearer","expires_in":3600}""") + } + else -> error("Unexpected auth request: ${request.url}") + } + } + + // ----------------------------------------------------------------------- + // Plugin integration — header injection and token caching + // ----------------------------------------------------------------------- + + @Test + fun `provider injects Authorization Bearer header into request`() = runTest { + val capturedAuth = mutableListOf() + val mcpClient = HttpClient(MockEngine { request -> + capturedAuth += request.headers[HttpHeaders.Authorization] ?: "" + respond("OK", HttpStatusCode.OK) + }) { + install(EnterpriseAuthProvider) { + clientId = "test-client" + assertionCallback = { _ -> "test-jag" } + authHttpClient = HttpClient(buildMockAuthEngine()) + } + } + + mcpClient.get("http://mcp.example.com/messages") + + capturedAuth shouldBe listOf("Bearer mcp-access-token") + } + + @Test + fun `provider caches token across multiple requests`() = runTest { + val tokenEndpointCalls = mutableListOf() + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(buildMockAuthEngine(tokenEndpointCalls)) + } + } + + mcpClient.get("http://mcp.example.com/messages") + mcpClient.get("http://mcp.example.com/messages") + mcpClient.get("http://mcp.example.com/messages") + + tokenEndpointCalls.size shouldBe 1 + } + + @Test + fun `invalidateCache forces re-fetch on next request`() = runTest { + var tokenCounter = 0 + val capturedAuth = mutableListOf() + + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"token_endpoint":"https://auth.example.com/token"}""") + "/token" -> { + tokenCounter++ + jsonOk("""{"access_token":"token-$tokenCounter","token_type":"Bearer","expires_in":3600}""") + } + else -> error("Unexpected: ${request.url}") + } + } + + val mcpClient = HttpClient(MockEngine { request -> + capturedAuth += request.headers[HttpHeaders.Authorization] ?: "" + respond("OK", HttpStatusCode.OK) + }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(authEngine) + } + } + + mcpClient.get("http://mcp.example.com/messages") // fetches token-1 + + val provider = mcpClient.plugin(EnterpriseAuthProvider) + provider.invalidateCache() + + mcpClient.get("http://mcp.example.com/messages") // fetches token-2 + + tokenCounter shouldBe 2 + capturedAuth[0] shouldBe "Bearer token-1" + capturedAuth[1] shouldBe "Bearer token-2" + } + + // ----------------------------------------------------------------------- + // Plugin integration — error propagation + // ----------------------------------------------------------------------- + + @Test + fun `provider discovery failure propagates as EnterpriseAuthException`() = runTest { + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> serverError() + "/.well-known/openid-configuration" -> serverError() + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(authEngine) + } + } + + shouldThrow { + mcpClient.get("http://mcp.example.com/messages") + } + } + + @Test + fun `provider assertion callback error propagates`() = runTest { + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"token_endpoint":"https://auth.example.com/token"}""") + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> throw RuntimeException("callback failed") } + authHttpClient = HttpClient(authEngine) + } + } + + shouldThrow { + mcpClient.get("http://mcp.example.com/messages") + }.message shouldBe "callback failed" + } + + // ----------------------------------------------------------------------- + // Plugin integration — assertion context + // ----------------------------------------------------------------------- + + @Test + fun `provider passes correct resourceUrl and authorizationServerUrl to callback`() = runTest { + var capturedContext: EnterpriseAuthAssertionContext? = null + + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"issuer":"https://auth.example.com","token_endpoint":"https://auth.example.com/token"}""") + "/token" -> + jsonOk("""{"access_token":"token","token_type":"Bearer","expires_in":3600}""") + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { ctx -> + capturedContext = ctx + "jag" + } + authHttpClient = HttpClient(authEngine) + } + } + + mcpClient.get("http://mcp.example.com/messages") + + capturedContext shouldNotBe null + capturedContext!!.resourceUrl shouldBe "http://mcp.example.com" + capturedContext!!.authorizationServerUrl shouldBe "https://auth.example.com" + } + + // ----------------------------------------------------------------------- + // EnterpriseAuthProvider.prepare — options validation + // ----------------------------------------------------------------------- + + @Test + fun `prepare throws when clientId is null`() { + shouldThrow { + EnterpriseAuthProvider.prepare { + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) + // clientId not set + } + }.message shouldContain "clientId" + } + + @Test + fun `prepare throws when assertionCallback is null`() { + shouldThrow { + EnterpriseAuthProvider.prepare { + clientId = "my-client" + authHttpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) + // assertionCallback not set + } + }.message shouldContain "assertionCallback" + } + + @Test + fun `provider throws when assertionCallback returns blank string`() = runTest { + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"token_endpoint":"https://auth.example.com/token"}""") + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> " " } // blank + authHttpClient = HttpClient(authEngine) + } + } + shouldThrow { + mcpClient.get("http://mcp.example.com/messages") + }.message shouldContain "blank" + } + + @Test + fun `provider token without expiresIn is cached indefinitely`() = runTest { + // When the server omits expires_in the token has no expiry and is kept + // in cache indefinitely (until explicitly invalidated). + val tokenEndpointCalls = mutableListOf() + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"token_endpoint":"https://auth.example.com/token"}""") + "/token" -> { + tokenEndpointCalls.add(tokenEndpointCalls.size + 1) + jsonOk("""{"access_token":"no-expiry-token","token_type":"Bearer"}""") + } + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(authEngine) + } + } + + mcpClient.get("http://mcp.example.com/messages") + mcpClient.get("http://mcp.example.com/messages") + mcpClient.get("http://mcp.example.com/messages") + + tokenEndpointCalls.size shouldBe 1 + } + + @Test + fun `provider respects custom expiryBuffer`() = runTest { + val tokenEndpointCalls = mutableListOf() + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"token_endpoint":"https://auth.example.com/token"}""") + "/token" -> { + tokenEndpointCalls.add(tokenEndpointCalls.size + 1) + // expires_in=1 with a 0s buffer — token should be cached until truly expired + jsonOk("""{"access_token":"token","token_type":"Bearer","expires_in":1}""") + } + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(authEngine) + expiryBuffer = 0.seconds // no buffer — token is valid until it actually expires + } + } + // Both requests should reuse the cached token (it hasn't actually expired yet) + mcpClient.get("http://mcp.example.com/messages") + mcpClient.get("http://mcp.example.com/messages") + + tokenEndpointCalls.size shouldBe 1 + } + + // ----------------------------------------------------------------------- + // Full end-to-end via plugin — header propagated correctly + // ----------------------------------------------------------------------- + + @Test + fun `full flow via plugin - Authorization header set and token is reused`() = runTest { + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"token_endpoint":"https://idp.example.com/token"}""") + "/token" -> + jsonOk("""{"access_token":"final-access-token","token_type":"Bearer","expires_in":3600}""") + else -> error("Unexpected: ${request.url}") + } + } + + val capturedAuthHeaders = mutableListOf() + val mcpEngine = MockEngine { request -> + capturedAuthHeaders += request.headers[HttpHeaders.Authorization] ?: "" + respond("OK", HttpStatusCode.OK) + } + + val mcpClient = HttpClient(mcpEngine) { + install(EnterpriseAuthProvider) { + clientId = "mcp-client" + assertionCallback = { _ -> "enterprise-jag" } + authHttpClient = HttpClient(authEngine) + } + } + + val response1 = mcpClient.get("http://mcp.example.com/sse") + val response2 = mcpClient.get("http://mcp.example.com/sse") + + response1.bodyAsText() shouldBe "OK" + response2.bodyAsText() shouldBe "OK" + capturedAuthHeaders.all { it == "Bearer final-access-token" }.shouldBeTrue() + } +} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt index fea1d54cc..a52922541 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt @@ -6,18 +6,17 @@ import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.MockRequestHandleScope import io.ktor.client.engine.mock.respond -import io.ktor.client.plugins.plugin import io.ktor.client.request.HttpResponseData -import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsText import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.content.TextContent import io.ktor.http.headersOf +import io.ktor.util.decodeBase64Bytes import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.time.Duration.Companion.minutes @@ -25,7 +24,7 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.TimeSource /** - * Tests for [EnterpriseAuth] and [EnterpriseAuthProvider]. + * Tests for [EnterpriseAuth] — discovery, JAG request, and token exchange utilities. */ class EnterpriseAuthTest { @@ -196,7 +195,9 @@ class EnterpriseAuthTest { } @Test - fun `requestJwtAuthorizationGrant wrong token_type throws`() = runTest { + fun `requestJwtAuthorizationGrant nonStandardTokenType succeeds`() = runTest { + // token_type is informational per RFC 8693 §2.2.1; non-N_A values must not be + // rejected so that conformant IdPs that omit or vary the field are accepted. val httpClient = mockHttpClient( "/token" to { jsonOk( @@ -204,14 +205,10 @@ class EnterpriseAuthTest { ) }, ) - val ex = shouldThrow { - EnterpriseAuth.requestJwtAuthorizationGrant( - RequestJwtAuthGrantOptions("https://idp.example.com/token", "id", "client"), - httpClient, - ) - } - ex.message shouldContain "token_type" - ex.message shouldContain "Bearer" + EnterpriseAuth.requestJwtAuthorizationGrant( + RequestJwtAuthGrantOptions("https://idp.example.com/token", "id", "client"), + httpClient, + ) shouldBe "jag" } @Test @@ -322,7 +319,13 @@ class EnterpriseAuthTest { val body = requestBodyText(request) body shouldContain "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer" body shouldContain "assertion=my-jag" - body shouldContain "client_id=my-client" + // client credentials must be in Basic Auth header, NOT in the request body + body shouldNotContain "client_id" + val authHeader = request.headers[HttpHeaders.Authorization] + authHeader shouldNotBe null + authHeader!! shouldContain "Basic " + val decoded = authHeader.removePrefix("Basic ").decodeBase64Bytes().decodeToString() + decoded shouldBe "my-client:" jsonOk( """{"access_token":"access-token-123","token_type":"Bearer","expires_in":3600}""", ) @@ -398,241 +401,4 @@ class EnterpriseAuthTest { response.expiresAt = TimeSource.Monotonic.markNow() - 1.seconds response.isExpired().shouldBeTrue() } - - // ----------------------------------------------------------------------- - // EnterpriseAuthProvider — plugin integration - // ----------------------------------------------------------------------- - - /** Builds a standard mock auth engine (discovery + token exchange) for provider tests. */ - private fun buildMockAuthEngine( - tokenEndpointCallTracker: MutableList = mutableListOf(), - accessTokenValue: String = "mcp-access-token", - ) = MockEngine { request -> - when (request.url.encodedPath) { - "/.well-known/oauth-authorization-server" -> - jsonOk("""{"issuer":"https://auth.example.com","token_endpoint":"https://auth.example.com/token"}""") - "/token" -> { - tokenEndpointCallTracker.add(tokenEndpointCallTracker.size + 1) - jsonOk("""{"access_token":"$accessTokenValue","token_type":"Bearer","expires_in":3600}""") - } - else -> error("Unexpected auth request: ${request.url}") - } - } - - @Test - fun `provider injects Authorization Bearer header into request`() = runTest { - val capturedAuth = mutableListOf() - val mcpClient = HttpClient(MockEngine { request -> - capturedAuth += request.headers[HttpHeaders.Authorization] ?: "" - respond("OK", HttpStatusCode.OK) - }) { - install(EnterpriseAuthProvider) { - clientId = "test-client" - assertionCallback = { _ -> "test-jag" } - authHttpClient = HttpClient(buildMockAuthEngine()) - } - } - - mcpClient.get("http://mcp.example.com/messages") - - capturedAuth shouldBe listOf("Bearer mcp-access-token") - } - - @Test - fun `provider caches token across multiple requests`() = runTest { - val tokenEndpointCalls = mutableListOf() - val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { - install(EnterpriseAuthProvider) { - clientId = "client" - assertionCallback = { _ -> "jag" } - authHttpClient = HttpClient(buildMockAuthEngine(tokenEndpointCalls)) - } - } - - mcpClient.get("http://mcp.example.com/messages") - mcpClient.get("http://mcp.example.com/messages") - mcpClient.get("http://mcp.example.com/messages") - - tokenEndpointCalls.size shouldBe 1 - } - - @Test - fun `invalidateCache forces re-fetch on next request`() = runTest { - var tokenCounter = 0 - val capturedAuth = mutableListOf() - - val authEngine = MockEngine { request -> - when (request.url.encodedPath) { - "/.well-known/oauth-authorization-server" -> - jsonOk("""{"token_endpoint":"https://auth.example.com/token"}""") - "/token" -> { - tokenCounter++ - jsonOk("""{"access_token":"token-$tokenCounter","token_type":"Bearer","expires_in":3600}""") - } - else -> error("Unexpected: ${request.url}") - } - } - - val mcpClient = HttpClient(MockEngine { request -> - capturedAuth += request.headers[HttpHeaders.Authorization] ?: "" - respond("OK", HttpStatusCode.OK) - }) { - install(EnterpriseAuthProvider) { - clientId = "client" - assertionCallback = { _ -> "jag" } - authHttpClient = HttpClient(authEngine) - } - } - - mcpClient.get("http://mcp.example.com/messages") // fetches token-1 - - val provider = mcpClient.plugin(EnterpriseAuthProvider) - provider.invalidateCache() - - mcpClient.get("http://mcp.example.com/messages") // fetches token-2 - - tokenCounter shouldBe 2 - capturedAuth[0] shouldBe "Bearer token-1" - capturedAuth[1] shouldBe "Bearer token-2" - } - - @Test - fun `provider discovery failure propagates as EnterpriseAuthException`() = runTest { - val authEngine = MockEngine { request -> - when (request.url.encodedPath) { - "/.well-known/oauth-authorization-server" -> serverError() - "/.well-known/openid-configuration" -> serverError() - else -> error("Unexpected: ${request.url}") - } - } - val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { - install(EnterpriseAuthProvider) { - clientId = "client" - assertionCallback = { _ -> "jag" } - authHttpClient = HttpClient(authEngine) - } - } - - shouldThrow { - mcpClient.get("http://mcp.example.com/messages") - } - } - - @Test - fun `provider assertion callback error propagates`() = runTest { - val authEngine = MockEngine { request -> - when (request.url.encodedPath) { - "/.well-known/oauth-authorization-server" -> - jsonOk("""{"token_endpoint":"https://auth.example.com/token"}""") - else -> error("Unexpected: ${request.url}") - } - } - val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { - install(EnterpriseAuthProvider) { - clientId = "client" - assertionCallback = { _ -> throw RuntimeException("callback failed") } - authHttpClient = HttpClient(authEngine) - } - } - - shouldThrow { - mcpClient.get("http://mcp.example.com/messages") - }.message shouldBe "callback failed" - } - - @Test - fun `provider passes correct resourceUrl and authorizationServerUrl to callback`() = runTest { - var capturedContext: EnterpriseAuthAssertionContext? = null - - val authEngine = MockEngine { request -> - when (request.url.encodedPath) { - "/.well-known/oauth-authorization-server" -> - jsonOk("""{"issuer":"https://auth.example.com","token_endpoint":"https://auth.example.com/token"}""") - "/token" -> - jsonOk("""{"access_token":"token","token_type":"Bearer","expires_in":3600}""") - else -> error("Unexpected: ${request.url}") - } - } - val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { - install(EnterpriseAuthProvider) { - clientId = "client" - assertionCallback = { ctx -> - capturedContext = ctx - "jag" - } - authHttpClient = HttpClient(authEngine) - } - } - - mcpClient.get("http://mcp.example.com/messages") - - capturedContext shouldNotBe null - capturedContext!!.resourceUrl shouldBe "http://mcp.example.com" - capturedContext!!.authorizationServerUrl shouldBe "https://auth.example.com" - } - - // ----------------------------------------------------------------------- - // EnterpriseAuthProvider.prepare — options validation - // ----------------------------------------------------------------------- - - @Test - fun `prepare throws when clientId is null`() { - shouldThrow { - EnterpriseAuthProvider.prepare { - assertionCallback = { _ -> "jag" } - authHttpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) - // clientId not set - } - }.message shouldContain "clientId" - } - - @Test - fun `prepare throws when assertionCallback is null`() { - shouldThrow { - EnterpriseAuthProvider.prepare { - clientId = "my-client" - authHttpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) - // assertionCallback not set - } - }.message shouldContain "assertionCallback" - } - - // ----------------------------------------------------------------------- - // Full end-to-end via plugin — header propagated correctly - // ----------------------------------------------------------------------- - - @Test - fun `full flow via plugin - Authorization header set and token is reused`() = runTest { - val authEngine = MockEngine { request -> - when (request.url.encodedPath) { - "/.well-known/oauth-authorization-server" -> - jsonOk("""{"token_endpoint":"https://idp.example.com/token"}""") - "/token" -> - jsonOk("""{"access_token":"final-access-token","token_type":"Bearer","expires_in":3600}""") - else -> error("Unexpected: ${request.url}") - } - } - - val capturedAuthHeaders = mutableListOf() - val mcpEngine = MockEngine { request -> - capturedAuthHeaders += request.headers[HttpHeaders.Authorization] ?: "" - respond("OK", HttpStatusCode.OK) - } - - val mcpClient = HttpClient(mcpEngine) { - install(EnterpriseAuthProvider) { - clientId = "mcp-client" - assertionCallback = { _ -> "enterprise-jag" } - authHttpClient = HttpClient(authEngine) - } - } - - val response1 = mcpClient.get("http://mcp.example.com/sse") - val response2 = mcpClient.get("http://mcp.example.com/sse") - - response1.bodyAsText() shouldBe "OK" - response2.bodyAsText() shouldBe "OK" - capturedAuthHeaders.all { it == "Bearer final-access-token" }.shouldBeTrue() - } } - From bb4aff3a0a5bef2287f191a137083a653281ddf3 Mon Sep 17 00:00:00 2001 From: Prachi Pandey Date: Tue, 17 Mar 2026 11:24:54 +0530 Subject: [PATCH 023/187] fix: re-throw CancellationException, internal set on expiresAt, data class options --- kotlin-sdk-client/api/kotlin-sdk-client.api | 36 ++++++++++++++++++- .../DiscoverAndRequestJwtAuthGrantOptions.kt | 2 +- .../kotlin/sdk/client/auth/EnterpriseAuth.kt | 4 +++ .../auth/ExchangeJwtBearerGrantOptions.kt | 2 +- .../auth/JwtBearerAccessTokenResponse.kt | 1 + .../client/auth/RequestJwtAuthGrantOptions.kt | 2 +- 6 files changed, 43 insertions(+), 4 deletions(-) diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index 02058b4ed..0642ea2e3 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -180,6 +180,17 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMeta public final class io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions { public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions; + public fun equals (Ljava/lang/Object;)Z public final fun getAudience ()Ljava/lang/String; public final fun getClientId ()Ljava/lang/String; public final fun getClientSecret ()Ljava/lang/String; @@ -188,6 +199,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndReq public final fun getIdpUrl ()Ljava/lang/String; public final fun getResource ()Ljava/lang/String; public final fun getScope ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth { @@ -257,11 +270,21 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth public final class io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions { public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions; + public fun equals (Ljava/lang/Object;)Z public final fun getAssertion ()Ljava/lang/String; public final fun getClientId ()Ljava/lang/String; public final fun getClientSecret ()Ljava/lang/String; public final fun getScope ()Ljava/lang/String; public final fun getTokenEndpoint ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse { @@ -313,7 +336,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAcces public final fun getScope ()Ljava/lang/String; public final fun getTokenType ()Ljava/lang/String; public final fun isExpired ()Z - public final fun setExpiresAt (Lkotlin/time/ComparableTimeMark;)V } public final synthetic class io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse$$serializer : kotlinx/serialization/internal/GeneratedSerializer { @@ -334,6 +356,16 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAcces public final class io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions { public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions; + public fun equals (Ljava/lang/Object;)Z public final fun getAudience ()Ljava/lang/String; public final fun getClientId ()Ljava/lang/String; public final fun getClientSecret ()Ljava/lang/String; @@ -341,5 +373,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuth public final fun getResource ()Ljava/lang/String; public final fun getScope ()Ljava/lang/String; public final fun getTokenEndpoint ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt index 08cd24e75..1d513268b 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt @@ -20,7 +20,7 @@ package io.modelcontextprotocol.kotlin.sdk.client.auth * @param resource Optional `resource` parameter for the token exchange request. * @param scope Optional `scope` parameter for the token exchange request. */ -public class DiscoverAndRequestJwtAuthGrantOptions( +public data class DiscoverAndRequestJwtAuthGrantOptions( public val idpUrl: String, public val idToken: String, public val clientId: String, diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt index 83c43bbb7..25b81c544 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt @@ -192,6 +192,8 @@ public object EnterpriseAuth { tokenResponse.accessToken!! } catch (e: EnterpriseAuthException) { throw e + } catch (e: CancellationException) { + throw e } catch (e: Exception) { throw EnterpriseAuthException("Failed to parse JAG token exchange response", e) } @@ -327,6 +329,8 @@ public object EnterpriseAuth { tokenResponse } catch (e: EnterpriseAuthException) { throw e + } catch (e: CancellationException) { + throw e } catch (e: Exception) { throw EnterpriseAuthException("Failed to parse JWT bearer grant exchange response", e) } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt index 30ebeda9d..5b2234c1e 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt @@ -20,7 +20,7 @@ package io.modelcontextprotocol.kotlin.sdk.client.auth * * @see RFC 7523 */ -public class ExchangeJwtBearerGrantOptions( +public data class ExchangeJwtBearerGrantOptions( public val tokenEndpoint: String, public val assertion: String, public val clientId: String, diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt index 0e2619d42..bc5ed1685 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt @@ -33,6 +33,7 @@ public class JwtBearerAccessTokenResponse( * Set by [EnterpriseAuth.exchangeJwtBearerGrant] using [kotlin.time.TimeSource.Monotonic]. */ public var expiresAt: ComparableTimeMark? = null + internal set /** * Returns `true` if this token has passed its [expiresAt] mark, or `false` if diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt index 7b65b8504..634e87656 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt @@ -17,7 +17,7 @@ package io.modelcontextprotocol.kotlin.sdk.client.auth * * @see RFC 8693 */ -public class RequestJwtAuthGrantOptions( +public data class RequestJwtAuthGrantOptions( public val tokenEndpoint: String, public val idToken: String, public val clientId: String, From c136ad9bbe7bc429015952389ee636f39c1f7ac0 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 18 Mar 2026 00:34:50 +0100 Subject: [PATCH 024/187] docs: context7 config file (#605) ## Motivation and Context - removed unnecessary badges from readme - add context7 json file ## How Has This Been Tested? ## Breaking Changes ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [ ] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [ ] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context --- README.md | 5 ++--- context7.json | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 context7.json diff --git a/README.md b/README.md index acb5c054c..3e65b26a4 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,12 @@ [![Maven Central](https://img.shields.io/maven-central/v/io.modelcontextprotocol/kotlin-sdk.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/io.modelcontextprotocol/kotlin-sdk) [![Build](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/build.yml) -[![Conformance Tests](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/conformance.yml/badge.svg)](https://github.com/modelcontextprotocol/kotlin-sdk/actions/workflows/conformance.yml) -[![codecov](https://codecov.io/github/modelcontextprotocol/kotlin-sdk/graph/badge.svg?token=O2CFHNSFYM)](https://codecov.io/github/modelcontextprotocol/kotlin-sdk) [![Kotlin](https://img.shields.io/badge/kotlin-2.2+-blueviolet.svg?logo=kotlin)](http://kotlinlang.org) [![Kotlin Multiplatform](https://img.shields.io/badge/Platforms-%20JVM%20%7C%20Wasm%2FJS%20%7C%20Native%20-blueviolet?logo=kotlin)](https://kotlinlang.org/docs/multiplatform.html) [![JVM](https://img.shields.io/badge/JVM-11+-red.svg?logo=jvm)](http://java.com) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/modelcontextprotocol/kotlin-sdk) + Kotlin Multiplatform SDK for the [Model Context Protocol](https://modelcontextprotocol.io). It enables Kotlin applications targeting JVM, Native, JS, and Wasm to implement MCP clients and servers using a @@ -899,6 +897,7 @@ MCP server and client implementations with various transports. See the [samples overview](./samples/README.md) for a comparison table and detailed descriptions. ## Documentation +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/modelcontextprotocol/kotlin-sdk) - [API Reference](https://kotlin.sdk.modelcontextprotocol.io/) - [Model Context Protocol documentation](https://modelcontextprotocol.io) diff --git a/context7.json b/context7.json new file mode 100644 index 000000000..a28c0fc67 --- /dev/null +++ b/context7.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://context7.com/schema/context7.json", + "projectTitle": "Model Context Protocol Kotlin SDK", + "description": "Kotlin Multiplatform SDK for MCP — client and server implementations", + "excludeFolders": [ + ".github", + "buildSrc", + "config", + "gradle/wrapper" + ], + "excludeFiles": [ + "CONTRIBUTING.md", + "LICENSE", + "SECURITY.md", + "codecov.yml", + "gradlew", + "gradlew.bat", + ".editorconfig", + "knit.properties", + "gradle.properties", + "settings.gradle.kts", + "conformance-baseline.yml", + "run-conformance.sh", + "detekt-baseline.xml", + "detekt-baseline-test.xml", + "detekt-baseline-main.xml", + "detekt-baseline-commonMainSourceSet.xml", + "detekt-baseline-commonTestSourceSet.xml" + ] +} \ No newline at end of file From 16dcc97339303ae27c4d8efb8b328d16676576cf Mon Sep 17 00:00:00 2001 From: Prachi Pandey Date: Wed, 18 Mar 2026 11:06:59 +0530 Subject: [PATCH 025/187] test: improve coverage for auth models and EnterpriseAuthProvider edge cases --- conformance-test/run-conformance.sh | 10 +- .../auth/enterpriseAuthProviderScenario.kt | 147 ++++++++++++++++++ .../sdk/conformance/auth/registration.kt | 6 + .../client/auth/EnterpriseAuthProviderTest.kt | 88 +++++++++++ .../sdk/client/auth/EnterpriseAuthTest.kt | 58 +++++++ 5 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/enterpriseAuthProviderScenario.kt diff --git a/conformance-test/run-conformance.sh b/conformance-test/run-conformance.sh index 8f15ac0d1..55fda457f 100755 --- a/conformance-test/run-conformance.sh +++ b/conformance-test/run-conformance.sh @@ -132,7 +132,15 @@ run_client_auth_suite() { --expected-failures "$SCRIPT_DIR/conformance-baseline.yml" \ "$@" || rc=$? - local extra_scenarios=("auth/client-credentials-jwt" "auth/client-credentials-basic" "auth/cross-app-access-complete-flow") + local extra_scenarios=( + "auth/client-credentials-jwt" + "auth/client-credentials-basic" + "auth/cross-app-access-complete-flow" + # Exercise EnterpriseAuthProvider plugin and discoverAndRequestJwtAuthorizationGrant + # using the same mock IdP/AS infrastructure as cross-app-access-complete-flow. + "auth/cross-app-access-enterprise-auth-provider" + "auth/cross-app-access-discover-and-request" + ) for scenario in "${extra_scenarios[@]}"; do npx "@modelcontextprotocol/conformance@$CONFORMANCE_VERSION" client \ --command "$CLIENT_DIST" \ diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/enterpriseAuthProviderScenario.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/enterpriseAuthProviderScenario.kt new file mode 100644 index 000000000..ed41ea8db --- /dev/null +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/enterpriseAuthProviderScenario.kt @@ -0,0 +1,147 @@ +package io.modelcontextprotocol.kotlin.sdk.conformance.auth + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.client.auth.DiscoverAndRequestJwtAuthGrantOptions +import io.modelcontextprotocol.kotlin.sdk.client.auth.EnterpriseAuth +import io.modelcontextprotocol.kotlin.sdk.client.auth.EnterpriseAuthProvider +import io.modelcontextprotocol.kotlin.sdk.client.auth.RequestJwtAuthGrantOptions +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation + +/** + * SEP-990 cross-app access flow exercised through [EnterpriseAuthProvider] as a Ktor plugin. + * + * Reads `client_id`, `client_secret`, `idp_id_token`, and `idp_token_endpoint` from + * the conformance context. Installs [EnterpriseAuthProvider] on the MCP HTTP client so + * that the plugin transparently handles: + * - MCP authorization server discovery via RFC 8414 + * - JAG retrieval via [EnterpriseAuth.requestJwtAuthorizationGrant] (RFC 8693) + * - JWT bearer grant exchange via [EnterpriseAuth.exchangeJwtBearerGrant] (RFC 7523) + * - Access token caching and proactive refresh + * + * Exercises: [EnterpriseAuthProvider], [RequestJwtAuthGrantOptions], + * [EnterpriseAuth.requestJwtAuthorizationGrant], [EnterpriseAuth.exchangeJwtBearerGrant]. + */ +internal suspend fun runCrossAppAccessViaEnterpriseAuthProvider(serverUrl: String) { + val ctx = conformanceContext() + val clientId = ctx.requiredString("client_id") + val clientSecret = ctx.requiredString("client_secret") + val idpIdToken = ctx.requiredString("idp_id_token") + val idpTokenEndpoint = ctx.requiredString("idp_token_endpoint") + + // Dedicated HTTP client used only for auth requests (discovery + token exchanges). + // Kept separate from the MCP HTTP client so its lifecycle is explicit. + val authHttpClient = HttpClient(CIO) { + install(SSE) + } + + // Main MCP HTTP client — EnterpriseAuthProvider transparently injects Bearer tokens. + val mcpHttpClient = HttpClient(CIO) { + install(SSE) + followRedirects = false + install(EnterpriseAuthProvider) { + this.clientId = clientId + this.clientSecret = clientSecret + this.authHttpClient = authHttpClient + assertionCallback = { assertionCtx -> + // Step 1 (RFC 8693): exchange the enterprise OIDC ID Token for a + // JWT Authorization Grant (ID-JAG) at the enterprise IdP. + EnterpriseAuth.requestJwtAuthorizationGrant( + RequestJwtAuthGrantOptions( + tokenEndpoint = idpTokenEndpoint, + idToken = idpIdToken, + clientId = clientId, + clientSecret = clientSecret, + audience = assertionCtx.authorizationServerUrl, + resource = assertionCtx.resourceUrl, + ), + authHttpClient, + ) + // Step 2 (RFC 7523): EnterpriseAuthProvider handles the JWT bearer + // grant exchange internally via EnterpriseAuth.exchangeJwtBearerGrant. + } + } + } + + authHttpClient.use { + mcpHttpClient.use { client -> + val transport = StreamableHttpClientTransport(client, serverUrl) + val mcpClient = Client( + clientInfo = Implementation("conformance-enterprise-auth-provider", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + mcpClient.connect(transport) + mcpClient.listTools() + mcpClient.close() + } + } +} + +/** + * SEP-990 cross-app access flow that exercises + * [EnterpriseAuth.discoverAndRequestJwtAuthorizationGrant] inside the + * [EnterpriseAuthProvider] assertion callback. + * + * The `idp_token_endpoint` from the conformance context is supplied as + * [DiscoverAndRequestJwtAuthGrantOptions.idpTokenEndpoint], which skips the RFC 8414 + * discovery round-trip while still exercising the combined discover-and-request code path. + * + * Exercises: [EnterpriseAuth.discoverAndRequestJwtAuthorizationGrant], + * [DiscoverAndRequestJwtAuthGrantOptions]. + */ +internal suspend fun runCrossAppAccessViaDiscoverAndRequest(serverUrl: String) { + val ctx = conformanceContext() + val clientId = ctx.requiredString("client_id") + val clientSecret = ctx.requiredString("client_secret") + val idpIdToken = ctx.requiredString("idp_id_token") + val idpTokenEndpoint = ctx.requiredString("idp_token_endpoint") + + val authHttpClient = HttpClient(CIO) { + install(SSE) + } + + val mcpHttpClient = HttpClient(CIO) { + install(SSE) + followRedirects = false + install(EnterpriseAuthProvider) { + this.clientId = clientId + this.clientSecret = clientSecret + this.authHttpClient = authHttpClient + assertionCallback = { assertionCtx -> + // discoverAndRequestJwtAuthorizationGrant is called with idpTokenEndpoint + // set explicitly so that RFC 8414 discovery is skipped; idpUrl is still + // required by the type but unused when idpTokenEndpoint is non-null. + EnterpriseAuth.discoverAndRequestJwtAuthorizationGrant( + DiscoverAndRequestJwtAuthGrantOptions( + idpUrl = extractOrigin(idpTokenEndpoint), + idpTokenEndpoint = idpTokenEndpoint, + idToken = idpIdToken, + clientId = clientId, + clientSecret = clientSecret, + audience = assertionCtx.authorizationServerUrl, + resource = assertionCtx.resourceUrl, + ), + authHttpClient, + ) + } + } + } + + authHttpClient.use { + mcpHttpClient.use { client -> + val transport = StreamableHttpClientTransport(client, serverUrl) + val mcpClient = Client( + clientInfo = Implementation("conformance-discover-and-request", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + mcpClient.connect(transport) + mcpClient.listTools() + mcpClient.close() + } + } +} diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt index 4964f20d2..738d64155 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/registration.kt @@ -29,4 +29,10 @@ fun registerAuthScenarios() { scenarioHandlers["auth/client-credentials-jwt"] = ::runClientCredentialsJwt scenarioHandlers["auth/client-credentials-basic"] = ::runClientCredentialsBasic scenarioHandlers["auth/cross-app-access-complete-flow"] = ::runCrossAppAccess + // SEP-990 scenarios that exercise the EnterpriseAuthProvider Ktor plugin and the + // discoverAndRequestJwtAuthorizationGrant combined call. + scenarioHandlers["auth/cross-app-access-enterprise-auth-provider"] = + ::runCrossAppAccessViaEnterpriseAuthProvider + scenarioHandlers["auth/cross-app-access-discover-and-request"] = + ::runCrossAppAccessViaDiscoverAndRequest } diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt index 8f33b3f52..0ba2f3908 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt @@ -295,6 +295,94 @@ class EnterpriseAuthProviderTest { tokenEndpointCalls.size shouldBe 1 } + @Test + fun `provider throws when discovery returns no token endpoint`() = runTest { + // Both well-known endpoints return metadata without a token_endpoint. + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{}""") + "/.well-known/openid-configuration" -> + jsonOk("""{"issuer":"https://auth.example.com"}""") + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(authEngine) + } + } + val ex = shouldThrow { + mcpClient.get("http://mcp.example.com/messages") + } + ex.message shouldContain "token_endpoint" + } + + @Test + fun `provider uses resource base URL as authorizationServerUrl when issuer is absent`() = runTest { + var capturedContext: EnterpriseAuthAssertionContext? = null + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + // Metadata without issuer — provider should fall back to resourceBaseUrl + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"token_endpoint":"https://auth.example.com/token"}""") + "/token" -> + jsonOk("""{"access_token":"tok","token_type":"Bearer","expires_in":3600}""") + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { ctx -> + capturedContext = ctx + "jag" + } + authHttpClient = HttpClient(authEngine) + } + } + + mcpClient.get("http://mcp.example.com/messages") + + capturedContext shouldNotBe null + // No issuer in metadata → authorizationServerUrl falls back to resource base URL + capturedContext!!.authorizationServerUrl shouldBe "http://mcp.example.com" + } + + @Test + fun `provider refreshes token when it is near expiry`() = runTest { + val tokenEndpointCalls = mutableListOf() + val authEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/.well-known/oauth-authorization-server" -> + jsonOk("""{"token_endpoint":"https://auth.example.com/token"}""") + "/token" -> { + tokenEndpointCalls.add(tokenEndpointCalls.size + 1) + // expires_in=1s — token is near-expiry relative to a 90s buffer + jsonOk("""{"access_token":"tok","token_type":"Bearer","expires_in":1}""") + } + else -> error("Unexpected: ${request.url}") + } + } + val mcpClient = HttpClient(MockEngine { respond("OK", HttpStatusCode.OK) }) { + install(EnterpriseAuthProvider) { + clientId = "client" + assertionCallback = { _ -> "jag" } + authHttpClient = HttpClient(authEngine) + // 90s buffer >> 1s expires_in → every request sees the cached token as near-expiry + expiryBuffer = 90.seconds + } + } + + mcpClient.get("http://mcp.example.com/messages") + mcpClient.get("http://mcp.example.com/messages") + + // Each request should have fetched a fresh token since cached one is always near-expiry + tokenEndpointCalls.size shouldBe 2 + } + @Test fun `provider respects custom expiryBuffer`() = runTest { val tokenEndpointCalls = mutableListOf() diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt index a52922541..f2c40db18 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt @@ -378,6 +378,64 @@ class EnterpriseAuthTest { ex.message shouldContain "HTTP 500" } + // ----------------------------------------------------------------------- + // Serialization coverage — all fields of the response models + // ----------------------------------------------------------------------- + + @Test + fun `JagTokenExchangeResponse deserializes scope and expiresIn`() = runTest { + val httpClient = mockHttpClient( + "/token" to { + jsonOk( + """{"access_token":"jag","issued_token_type":"urn:ietf:params:oauth:token-type:id-jag","token_type":"N_A","scope":"read write","expires_in":600}""", + ) + }, + ) + // Run through requestJwtAuthorizationGrant so the response model is exercised + EnterpriseAuth.requestJwtAuthorizationGrant( + RequestJwtAuthGrantOptions( + tokenEndpoint = "https://idp.example.com/token", + idToken = "id", + clientId = "client", + ), + httpClient, + ) shouldBe "jag" + } + + @Test + fun `AuthServerMetadata deserializes jwksUri`() = runTest { + val httpClient = mockHttpClient( + "/.well-known/oauth-authorization-server" to { + jsonOk( + """{"issuer":"https://auth.example.com","token_endpoint":"https://auth.example.com/token","authorization_endpoint":"https://auth.example.com/authorize","jwks_uri":"https://auth.example.com/jwks"}""", + ) + }, + ) + val metadata = EnterpriseAuth.discoverAuthServerMetadata("https://auth.example.com", httpClient) + metadata.jwksUri shouldBe "https://auth.example.com/jwks" + } + + @Test + fun `JwtBearerAccessTokenResponse deserializes scope and refreshToken`() = runTest { + val httpClient = mockHttpClient( + "/token" to { + jsonOk( + """{"access_token":"at","token_type":"Bearer","expires_in":3600,"scope":"mcp:read","refresh_token":"rt-ignored"}""", + ) + }, + ) + val response = EnterpriseAuth.exchangeJwtBearerGrant( + ExchangeJwtBearerGrantOptions( + tokenEndpoint = "https://auth.example.com/token", + assertion = "jag", + clientId = "client", + ), + httpClient, + ) + response.scope shouldBe "mcp:read" + response.refreshToken shouldBe "rt-ignored" + } + // ----------------------------------------------------------------------- // JwtBearerAccessTokenResponse.isExpired // ----------------------------------------------------------------------- From 4cfc719178105f945094233d39ad5417873fcd19 Mon Sep 17 00:00:00 2001 From: Prachi Pandey Date: Wed, 18 Mar 2026 11:13:27 +0530 Subject: [PATCH 026/187] fix: align conformance scenarios with existing pattern (followRedirects, single use block) --- .../auth/enterpriseAuthProviderScenario.kt | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/enterpriseAuthProviderScenario.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/enterpriseAuthProviderScenario.kt index ed41ea8db..f3ced6328 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/enterpriseAuthProviderScenario.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/auth/enterpriseAuthProviderScenario.kt @@ -34,13 +34,11 @@ internal suspend fun runCrossAppAccessViaEnterpriseAuthProvider(serverUrl: Strin val idpIdToken = ctx.requiredString("idp_id_token") val idpTokenEndpoint = ctx.requiredString("idp_token_endpoint") - // Dedicated HTTP client used only for auth requests (discovery + token exchanges). - // Kept separate from the MCP HTTP client so its lifecycle is explicit. val authHttpClient = HttpClient(CIO) { install(SSE) + followRedirects = false } - // Main MCP HTTP client — EnterpriseAuthProvider transparently injects Bearer tokens. val mcpHttpClient = HttpClient(CIO) { install(SSE) followRedirects = false @@ -68,17 +66,15 @@ internal suspend fun runCrossAppAccessViaEnterpriseAuthProvider(serverUrl: Strin } } - authHttpClient.use { - mcpHttpClient.use { client -> - val transport = StreamableHttpClientTransport(client, serverUrl) - val mcpClient = Client( - clientInfo = Implementation("conformance-enterprise-auth-provider", "1.0.0"), - options = ClientOptions(capabilities = ClientCapabilities()), - ) - mcpClient.connect(transport) - mcpClient.listTools() - mcpClient.close() - } + mcpHttpClient.use { client -> + val transport = StreamableHttpClientTransport(client, serverUrl) + val mcpClient = Client( + clientInfo = Implementation("conformance-enterprise-auth-provider", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + mcpClient.connect(transport) + mcpClient.listTools() + mcpClient.close() } } @@ -103,6 +99,7 @@ internal suspend fun runCrossAppAccessViaDiscoverAndRequest(serverUrl: String) { val authHttpClient = HttpClient(CIO) { install(SSE) + followRedirects = false } val mcpHttpClient = HttpClient(CIO) { @@ -132,16 +129,14 @@ internal suspend fun runCrossAppAccessViaDiscoverAndRequest(serverUrl: String) { } } - authHttpClient.use { - mcpHttpClient.use { client -> - val transport = StreamableHttpClientTransport(client, serverUrl) - val mcpClient = Client( - clientInfo = Implementation("conformance-discover-and-request", "1.0.0"), - options = ClientOptions(capabilities = ClientCapabilities()), - ) - mcpClient.connect(transport) - mcpClient.listTools() - mcpClient.close() - } + mcpHttpClient.use { client -> + val transport = StreamableHttpClientTransport(client, serverUrl) + val mcpClient = Client( + clientInfo = Implementation("conformance-discover-and-request", "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + mcpClient.connect(transport) + mcpClient.listTools() + mcpClient.close() } } From 2542c9cae152b0800b7f4ca1957635545b397f0a Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 18 Mar 2026 17:53:26 +0100 Subject: [PATCH 027/187] feat: add mcp documentation skill for spec-aware development (#609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Claude Code skill (`.claude/skills/mcp-docs/`) that provides live MCP documentation and specification context ## Motivation and Context The modelcontextprotocol.io site provides both an [MCP server](https://modelcontextprotocol.io/mcp) and an [llms.txt](https://modelcontextprotocol.io/llms.txt) index for machine-readable access to the documentation. This skill leverages both to give Claude live access to the current spec during development. ## How Has This Been Tested? The skill was evaluated on 3 realistic scenarios (implementation, code review, spec question) comparing with-skill vs without-skill baseline: - **Pass rate**: 100% vs 67% — skill consistently cites spec sources and uses RFC 2119 language - **Token efficiency**: 41% fewer tokens on average (44k vs 75k) — goes directly to spec instead of reverse-engineering from codebase - **Accuracy**: with-skill discovered URL mode elicitation (new in spec 2025-11-25) that baseline missed entirely - **Description triggering**: optimized through 5 iterations of automated trigger evaluation against 20 test prompts ## Breaking Changes None ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .claude/skills/mcp-docs/SKILL.md | 76 ++++++++++++++++++++++++++++++++ .gitignore | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/mcp-docs/SKILL.md diff --git a/.claude/skills/mcp-docs/SKILL.md b/.claude/skills/mcp-docs/SKILL.md new file mode 100644 index 000000000..074e6f962 --- /dev/null +++ b/.claude/skills/mcp-docs/SKILL.md @@ -0,0 +1,76 @@ +--- +name: mcp-docs +description: > + Fetch live MCP specification and docs from modelcontextprotocol.io. Training data may be + outdated — default to using this skill when the query touches MCP protocol in any way. + Trigger for: implementing or debugging MCP features (sampling, elicitation, completion, tools, + resources, prompts, transports); SEPs or spec requirements; MCP transport behavior (SSE, + Streamable HTTP, reconnection, retry); authorization/OAuth; conformance test failures; + capability negotiation; message schemas. Skip only for pure Kotlin, build system, or + refactoring tasks with zero MCP protocol dependency. When in doubt, trigger. +user_invocable: true +--- + +# MCP Documentation Skill + +Provides access to the official MCP specification and docs from modelcontextprotocol.io. The spec evolves faster than training data — always consult live documentation for protocol specifics like message schemas, required fields, capability negotiation, error handling, and transport requirements. + +## When to use + +**Proactively** (without being asked): +- Implementing a feature defined in the MCP spec +- Reviewing code that implements MCP protocol behavior +- Fixing conformance test failures + +**On demand** (user asks or invokes `/mcp-docs`): +- Questions about how a specific MCP feature works +- Clarifying spec ambiguities +- Looking up SEPs (Specification Enhancement Proposals) + +## How to fetch documentation + +### Strategy: MCP tool first, llms.txt fallback + +**Step 1 — Try the MCP search tool.** If `search_model_context_protocol` is available, use it: + +``` +search_model_context_protocol(query: "") +``` + +One call, semantically ranked results. Fastest path. + +**Step 2 — Fallback to llms.txt + WebFetch.** If the MCP tool is not available: + +1. Fetch the documentation index: + ``` + WebFetch("https://modelcontextprotocol.io/llms.txt", "Return the full content as-is") + ``` + +2. Identify the relevant page URL(s) from the index. Key sections: + - **Specification**: `specification/...` — protocol details, message formats, transports, lifecycle + - **SEPs**: `seps/...` — specification enhancement proposals + - **Learning**: `docs/learn/...` — architecture, concepts + - **Development**: `docs/develop/...` — building clients/servers + +3. Fetch the specific page: + ``` + WebFetch("", "Return the full content of this documentation page") + ``` + +### Version handling + +Always use the latest spec version by default. Only use a specific version or draft when the user explicitly requests it (e.g., "check the 2025-03-26 spec" or "what does the draft say"). + +### Search guidance + +- Protocol message formats, required fields, capability negotiation → **Specification** section +- Feature motivation, design rationale → relevant **SEP** +- High-level concepts and architecture → **Learning** resources +- Use your judgment on query count. A field-level question may need one page; implementing a feature may need 2-3. + +## Presenting information + +- **Always cite sources.** Include links to spec pages used: `(source: )` +- **Spec over training data** for specific protocol details (schemas, fields, error codes, capabilities). Training data is fine for general MCP concepts. +- **Preserve RFC 2119 language.** The spec uses MUST/SHOULD/MAY — keep this distinction when explaining requirements. +- **Be concise.** Focus on the details relevant to the user's task. Avoid dumping entire spec pages — extract what matters for the specific question or implementation at hand. diff --git a/.gitignore b/.gitignore index 805579c50..d9f0fdec0 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,7 @@ node_modules dist ### SWE agents ### -.claude/ +.claude/settings.local.json .junie/ ### Conformance test results ### From ee6561c18582ed5992d2f45de218a6aec2fef9d9 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:30:22 +0200 Subject: [PATCH 028/187] feat: #502 resource templates with basic template matcher (#606) --- conformance-test/conformance-baseline.yml | 3 +- .../sdk/conformance/ConformanceResources.kt | 11 +- .../kotlin/sdk/client/ClientTest.kt | 5 +- .../kotlin/AbstractPromptIntegrationTest.kt | 16 +- .../kotlin/AbstractResourceIntegrationTest.kt | 7 +- .../sdk/server/ServerResourceTemplateTest.kt | 222 ++++++++++++++++ kotlin-sdk-core/api/kotlin-sdk-core.api | 35 +++ .../kotlin/sdk/shared/Protocol.kt | 21 +- .../kotlin/sdk/types/McpException.kt | 7 +- .../kotlin/sdk/types/jsonRpc.kt | 3 + .../sdk/utils/PathSegmentTemplateMatcher.kt | 157 ++++++++++++ .../sdk/utils/ResourceTemplateMatcher.kt | 67 +++++ .../kotlin/sdk/utils/UriUtils.kt | 16 ++ .../PathSegmentTemplateMatcherSecurityTest.kt | 221 ++++++++++++++++ .../utils/PathSegmentTemplateMatcherTest.kt | 236 ++++++++++++++++++ .../kotlin/sdk/utils/UriUtils.js.kt | 16 ++ .../kotlin/sdk/utils/UriUtils.jvm.kt | 14 ++ .../kotlin/sdk/utils/UriUtils.native.kt | 3 + .../kotlin/sdk/utils/UriUtils.wasmJs.kt | 4 + kotlin-sdk-server/api/kotlin-sdk-server.api | 9 + .../kotlin/sdk/server/Feature.kt | 17 ++ .../kotlin/sdk/server/Server.kt | 158 ++++++++++-- 22 files changed, 1189 insertions(+), 59 deletions(-) create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt create mode 100644 kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt create mode 100644 kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt create mode 100644 kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.kt create mode 100644 kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherSecurityTest.kt create mode 100644 kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherTest.kt create mode 100644 kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.js.kt create mode 100644 kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.jvm.kt create mode 100644 kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.native.kt create mode 100644 kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.wasmJs.kt diff --git a/conformance-test/conformance-baseline.yml b/conformance-test/conformance-baseline.yml index 378884748..337d8ce61 100644 --- a/conformance-test/conformance-baseline.yml +++ b/conformance-test/conformance-baseline.yml @@ -1,7 +1,6 @@ # Conformance test baseline - expected failures # Add entries here as tests are identified as known SDK limitations -server: - - resources-templates-read +server: [] client: - elicitation-sep1034-client-defaults diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt index a09903771..89d83f68d 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceResources.kt @@ -45,18 +45,17 @@ fun Server.registerConformanceResources() { } // 3. Template resource - // Note: The SDK does not currently support addResourceTemplate(). - // Register as a static resource; template listing is handled separately. - addResource( - uri = "test://template/{id}/data", + addResourceTemplate( + uriTemplate = "test://template/{id}/data", name = "template", description = "A template resource for testing", mimeType = "application/json", - ) { request -> + ) { request, variables -> + val id = variables["id"] ReadResourceResult( listOf( TextResourceContents( - text = "content for ${request.uri}", + text = """{"id": "$id"}""", uri = request.uri, mimeType = "application/json", ), diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index 0726fa829..b117dbfb4 100644 --- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -1,5 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.client +import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.ServerSession @@ -266,8 +267,8 @@ class ClientTest { client.connect(failingTransport) } - assertEquals(-32600, exception.code) - assertEquals("MCP error -32600: Invalid Request", exception.message) + exception.code shouldBe -32600 + exception.message shouldBe "Invalid Request" assertTrue(closed) } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt index 9380401d8..d14ae88e8 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams @@ -8,7 +9,6 @@ import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult import io.modelcontextprotocol.kotlin.sdk.types.McpException import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage -import io.modelcontextprotocol.kotlin.sdk.types.RPCError import io.modelcontextprotocol.kotlin.sdk.types.Role import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.TextContent @@ -688,13 +688,13 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { } } - val expectedMessage = "MCP error -32603: Prompt not found: non-existent-prompt" + val expectedMessage = "Prompt not found: non-existent-prompt" - assertEquals( - RPCError.ErrorCode.INTERNAL_ERROR, - exception.code, - "Exception code should be INTERNAL_ERROR: ${RPCError.ErrorCode.INTERNAL_ERROR}", - ) - assertEquals(expectedMessage, exception.message, "Unexpected error message for non-existent prompt") + withClue("Exception code should be INTERNAL_ERROR: -32603") { + exception.code shouldBe -32603 + } + withClue("Unexpected error message for non-existent prompt") { + exception.message shouldBe expectedMessage + } } } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt index 40b955204..4fc53a52c 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt @@ -213,14 +213,11 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { } } - val expectedMessage = "MCP error -32603: Resource not found: test://nonexistent.txt" - assertEquals( - RPCError.ErrorCode.INTERNAL_ERROR, + RPCError.ErrorCode.RESOURCE_NOT_FOUND, exception.code, - "Exception code should be INTERNAL_ERROR: ${RPCError.ErrorCode.INTERNAL_ERROR}", + "Exception code should be RESOURCE_NOT_FOUND: ${RPCError.ErrorCode.RESOURCE_NOT_FOUND}", ) - assertEquals(expectedMessage, exception.message, "Unexpected error message for invalid resource URI") } @Test diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt new file mode 100644 index 000000000..c8ace1915 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt @@ -0,0 +1,222 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.maps.shouldContainKey +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class ServerResourceTemplateTest : AbstractServerFeaturesTest() { + + override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(listChanged = null, subscribe = null), + ) + + @Test + fun `listResourceTemplates should return registered templates`() = runTest { + server.addResourceTemplate("test://data/{id}", "Test Data", mimeType = "text/plain") { _, _ -> + ReadResourceResult(listOf(TextResourceContents("content", "test://data/1"))) + } + + val result = client.listResourceTemplates(ListResourceTemplatesRequest()) + + result.resourceTemplates shouldHaveSize 1 + result.resourceTemplates[0] shouldNotBeNull { + uriTemplate shouldBe "test://data/{id}" + name shouldBe "Test Data" + mimeType shouldBe "text/plain" + } + } + + @Test + fun `listResourceTemplates should return empty list when none registered`() = runTest { + val result = client.listResourceTemplates(ListResourceTemplatesRequest()) + + result.resourceTemplates.shouldBeEmpty() + } + + @Test + fun `readResource should match URI against template and invoke handler`() = runTest { + server.addResourceTemplate("test://items/{itemId}", "Item", mimeType = "text/plain") { request, variables -> + val itemId = variables["itemId"] ?: "unknown" + ReadResourceResult( + listOf(TextResourceContents(text = "item=$itemId", uri = request.uri, mimeType = "text/plain")), + ) + } + + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://items/42"))) + + result.contents shouldBe + listOf(TextResourceContents(uri = "test://items/42", mimeType = "text/plain", text = "item=42")) + } + + @Test + fun `readResource should extract multiple URI template variables`() = runTest { + val capturedVars = CompletableDeferred>() + server.addResourceTemplate( + uriTemplate = "test://users/{userId}/posts/{postId}", + name = "User Post", + mimeType = "text/plain", + ) { _, variables -> + capturedVars.complete(variables) + ReadResourceResult(listOf(TextResourceContents("ok", "test://users/alice/posts/99"))) + } + + client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://users/alice/posts/99"))) + + val vars = capturedVars.await() + vars shouldContainKey "userId" + vars shouldContainKey "postId" + vars["userId"] shouldBe "alice" + vars["postId"] shouldBe "99" + } + + @Test + fun `readResource should prefer exact resource match over template`() = runTest { + var exactHandlerCalled = false + server.addResource("test://items/special", "Special Item", "An exact resource") { + exactHandlerCalled = true + ReadResourceResult(listOf(TextResourceContents("exact", "test://items/special"))) + } + server.addResourceTemplate("test://items/{itemId}", "Item Template") { _, _ -> + ReadResourceResult(listOf(TextResourceContents("template", "test://items/special"))) + } + + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://items/special"))) + + exactHandlerCalled shouldBe true + (result.contents[0] as TextResourceContents).text shouldBe "exact" + } + + @Test + fun `readResource should select most specific template when multiple match`() = runTest { + // "test://users/profile" has more literal chars than "test://users/{id}" — should win + server.addResourceTemplate("test://users/{id}", "Generic User") { _, variables -> + ReadResourceResult(listOf(TextResourceContents("generic:${variables["id"]}", "test://users/profile"))) + } + server.addResourceTemplate("test://users/profile", "Profile") { _, _ -> + ReadResourceResult(listOf(TextResourceContents("profile-page", "test://users/profile"))) + } + + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://users/profile"))) + + (result.contents[0] as TextResourceContents).text shouldBe "profile-page" + } + + @Test + fun `readResource should return RESOURCE_NOT_FOUND error when no match`() = runTest { + val exception = assertThrows { + client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://nonexistent/uri"))) + } + + exception.code shouldBe RPCError.ErrorCode.RESOURCE_NOT_FOUND + } + + @Test + fun `resourceTemplates property should reflect registered templates`() { + server.addResourceTemplate(ResourceTemplate("test://a/{x}", "A")) { _, _ -> + ReadResourceResult(emptyList()) + } + server.addResourceTemplate(ResourceTemplate("test://b/{y}", "B")) { _, _ -> + ReadResourceResult(emptyList()) + } + + val templates = server.resourceTemplates + + templates shouldBe listOf( + ResourceTemplate("test://a/{x}", "A"), + ResourceTemplate("test://b/{y}", "B"), + ) + } + + @Test + fun `removeResourceTemplate should remove a registered template`() { + server.addResourceTemplate("test://items/{id}", "Item") { _, _ -> + ReadResourceResult(emptyList()) + } + + val removed = server.removeResourceTemplate("test://items/{id}") + + removed shouldBe true + server.resourceTemplates.size shouldBe 0 + } + + @Test + fun `removeResourceTemplate should return false when template does not exist`() { + val removed = server.removeResourceTemplate("test://nonexistent/{id}") + + removed shouldBe false + } + + @Test + fun `addResourceTemplate should throw when resources capability is not supported`() { + val noResourcesServer = Server( + serverInfo = Implementation("test", "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) + + assertThrows { + noResourcesServer.addResourceTemplate("test://{id}", "Test") { _, _ -> + ReadResourceResult(emptyList()) + } + } + } + + @Test + fun `addResourceTemplate with ResourceTemplate object should register correctly`() = runTest { + val template = ResourceTemplate( + uriTemplate = "test://docs/{section}", + name = "Documentation", + description = "API docs", + mimeType = "text/html", + ) + server.addResourceTemplate(template) { request, variables -> + val section = variables["section"] ?: "index" + ReadResourceResult( + listOf(TextResourceContents("docs for $section", request.uri, mimeType = "text/html")), + ) + } + + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://docs/api"))) + + result.contents shouldHaveSize 1 + (result.contents[0] as TextResourceContents).text shouldBe "docs for api" + } + + @Test + fun `listResourceTemplates should include description from template`() = runTest { + server.addResourceTemplate( + uriTemplate = "test://data/{id}", + name = "Data", + description = "Parameterized data resource", + mimeType = "application/json", + ) { _, _ -> + ReadResourceResult(emptyList()) + } + + val result = client.listResourceTemplates(ListResourceTemplatesRequest()) + + result.resourceTemplates shouldBe listOf( + ResourceTemplate( + name = "Data", + uriTemplate = "test://data/{id}", + description = "Parameterized data resource", + mimeType = "application/json", + ), + ) + } +} diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index af22323e2..dc3da3e44 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -2713,6 +2713,7 @@ public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/ty } public final class io/modelcontextprotocol/kotlin/sdk/types/McpException : java/lang/Exception { + public fun (I)V public fun (ILjava/lang/String;)V public fun (ILjava/lang/String;Lkotlinx/serialization/json/JsonElement;)V public fun (ILjava/lang/String;Lkotlinx/serialization/json/JsonElement;Ljava/lang/Throwable;)V @@ -3310,6 +3311,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RPCError$ErrorCode { public static final field METHOD_NOT_FOUND I public static final field PARSE_ERROR I public static final field REQUEST_TIMEOUT I + public static final field RESOURCE_NOT_FOUND I } public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { @@ -4731,3 +4733,36 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/WithMeta$DefaultImpl public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/WithMeta;)Lkotlinx/serialization/json/JsonObject; } +public final class io/modelcontextprotocol/kotlin/sdk/utils/MatchResult { + public fun (Ljava/util/Map;I)V + public fun equals (Ljava/lang/Object;)Z + public final fun getScore ()I + public final fun getVariables ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher : io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;I)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;II)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun getFactory ()Lio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory; + public fun getResourceTemplate ()Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate; + public fun match (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/utils/MatchResult; +} + +public final class io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher$Companion { + public final fun getFactory ()Lio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher { + public abstract fun getResourceTemplate ()Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate; + public abstract fun match (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/utils/MatchResult; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory { + public abstract fun create (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;)Lio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher; +} + diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index e9d833a2e..0e3db35cd 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -39,6 +39,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.encodeToJsonElement +import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -312,6 +313,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio return } + @Suppress("TooGenericExceptionCaught", "InstanceOfCheckForException") try { val result = handler(request, RequestHandlerExtra()) logger.trace { "Request handled successfully: ${request.method} (id: ${request.id})" } @@ -322,19 +324,20 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio result = result ?: EmptyResult(), ), ) + } catch (e: CancellationException) { + throw e } catch (cause: Throwable) { logger.error(cause) { "Error handling request: ${request.method} (id: ${request.id})" } try { - transport?.send( - JSONRPCError( - id = request.id, - error = RPCError( - code = RPCError.ErrorCode.INTERNAL_ERROR, - message = cause.message ?: "Internal error", - ), - ), - ) + val rpcError = if (cause is McpException) { + RPCError(code = cause.code, message = cause.message.orEmpty(), data = cause.data) + } else { + RPCError(code = RPCError.ErrorCode.INTERNAL_ERROR, message = cause.message ?: "Internal error") + } + transport?.send(JSONRPCError(id = request.id, error = rpcError)) + } catch (e: CancellationException) { + throw e } catch (sendError: Throwable) { logger.error(sendError) { "Failed to send error response for request: ${request.method} (id: ${request.id})" diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt index f63f4a9b7..6b1249274 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt @@ -8,12 +8,13 @@ import kotlin.jvm.JvmOverloads * * @property code The MCP/JSON‑RPC error code. * @property data Optional additional error payload as a JSON element; `null` when not provided. - * @param message The error message. + * @param message The error message. Used verbatim as [Exception.message] — no error code prefix is prepended. + * Defaults to `"MCP error $code"` when not provided. * @param cause The original cause. */ public class McpException @JvmOverloads public constructor( public val code: Int, - message: String, + message: String = "MCP error $code", public val data: JsonElement? = null, cause: Throwable? = null, -) : Exception("MCP error $code: $message", cause) +) : Exception(message, cause) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt index 68b2f0274..6a53d350f 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt @@ -251,6 +251,9 @@ public data class RPCError(val code: Int, val message: String, val data: JsonEle /** Request timed out */ public const val REQUEST_TIMEOUT: Int = -32001 + /** Resource not found */ + public const val RESOURCE_NOT_FOUND: Int = -32002 + // Standard JSON-RPC 2.0 error codes /** Invalid JSON was received */ diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt new file mode 100644 index 000000000..c5619d597 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt @@ -0,0 +1,157 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.http.decodeURLPart +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate +import kotlinx.collections.immutable.toImmutableMap +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic + +// Max URL/path length to prevent DoS or unexpected large payloads. +private const val MAX_URL_LENGTH = 2048 + +// Max template/uri depth to prevent overly nested or complex templates. +private const val MAX_DEPTH = 50 + +// Literal segments are more specific than variable captures. +private const val LITERAL_MATCH_SCORE = 2 +private const val VARIABLE_MATCH_SCORE = 1 + +/** + * A [ResourceTemplateMatcher] that matches resource URIs against an RFC 6570 Level 1 + * URI template by splitting both the URI and the template on `/` and comparing each + * segment in order. + * + * ### Supported template syntax + * + * Only RFC 6570 **Level 1** is supported: simple `{variable}` expressions where the + * entire path segment is a single variable. Operator expressions (`{+var}`, `{#var}`, + * `{.var}`, `{/var}`, etc.) and multi-variable expressions (`{a,b}`) are **not** + * recognized — segments containing them are treated as literals. + * + * ### Matching rules + * + * - The URI and template must have the same number of `/`-delimited segments. + * - Literal segments must match exactly (after percent-decoding the URI segment). + * - `{variable}` segments capture the percent-decoded URI segment value. + * - Query strings and fragments (`?`, `#`) are **not** stripped — they become part of + * the captured variable value for the segment that contains them. + * + * ### Specificity scoring + * + * When multiple templates match the same URI, each matched literal segment contributes + * 2 points and each variable capture contributes 1 point. The highest-scoring match wins. + * + * ### Safety limits + * + * - URIs longer than [maxUriLength] characters are rejected. + * - Templates and URIs with more than [maxDepth] segments are rejected. + * + * ### Security contract for handler authors + * + * Values in [MatchResult.variables] are attacker-controlled strings extracted from + * the incoming URI. They are percent-decoded (one pass only) before being returned. + * Handlers **must** treat them as untrusted input and validate or sanitize them + * before using them to construct file paths, database queries, downstream URLs, or + * any other security-sensitive operation. + * + * In particular: + * - A decoded value may contain `/`, `..`, null bytes (`\u0000`), `?`, or `#`. + * - `%252F` in the URI becomes `%2F` in the variable — exactly one decode pass is applied. + * + * ### Platform limitations + * + * Dot-segment normalization (resolving `..` and `.` in the URI path) is performed + * on JVM and JS targets using the platform-native URI parser. On native and WASM targets + * no normalizer is available, so dot-segment traversal is **not** mitigated at this + * layer — handlers on those targets must normalize paths themselves. + * + * @param resourceTemplate The resource template to match against. Must follow RFC 6570 Level 1 syntax. + * @param maxUriLength Maximum allowed length for incoming URIs. Defaults to 2048. + * @param maxDepth Maximum allowed segment count for the template/uri. Defaults to 50. + * + * @property resourceTemplate The resource template against which resource URIs will be matched. + */ +public class PathSegmentTemplateMatcher @JvmOverloads constructor( + override val resourceTemplate: ResourceTemplate, + private val maxDepth: Int = MAX_DEPTH, + private val maxUriLength: Int = MAX_URL_LENGTH, +) : ResourceTemplateMatcher { + + public companion object { + /** + * A [ResourceTemplateMatcherFactory] that creates [PathSegmentTemplateMatcher] instances + * with default limits. Pass this to [io.modelcontextprotocol.kotlin.sdk.server.ServerOptions] + * to use path-segment matching, or supply a custom factory to override the matching strategy. + */ + @JvmStatic + public val factory: ResourceTemplateMatcherFactory = ResourceTemplateMatcherFactory { + PathSegmentTemplateMatcher(it) + } + + @JvmStatic + private val logger = KotlinLogging.logger {} + } + + private val templateParts: List + + // Maps segment index to variable name; indices absent from this map are literal segments. + private val variableIndices: Map + + init { + val template = resourceTemplate.uriTemplate + require(template.isNotBlank()) { "Resource template cannot be blank" } + templateParts = template.trim('/').split("/") + require(templateParts.size <= maxDepth) { + "Template is too complex (max depth=$maxDepth)" + } + + val vars = mutableMapOf() + for (i in templateParts.indices) { + val segment = templateParts[i] + if (segment.startsWith("{") && segment.endsWith("}")) { + val name = segment.removeSurrounding("{", "}").trim() + require(name.isNotEmpty()) { "Invalid variable name in template: $segment" } + vars[i] = name + } + } + variableIndices = vars.toImmutableMap() + } + + @Suppress("ReturnCount") + override fun match(resourceUri: String): MatchResult? { + if (resourceUri.length > maxUriLength) { + logger.debug { "URL is too long (max=$maxUriLength)" } + return null + } + + // Resolve dot-segments (e.g. /a/../b → /a/b) before splitting. + // Prevents traversal attacks on JVM/JS; no-op on native/WASM targets. + val normalized = normalizeUri(resourceUri) + + val urlParts = normalized.trim('/').split("/") + if (urlParts.size > maxDepth) { + logger.debug { "URI has too many segments (max=$maxDepth)" } + return null + } + if (urlParts.size != templateParts.size) return null + + val variables = mutableMapOf() + var score = 0 + + for (i in templateParts.indices) { + val urlSegment = urlParts[i].decodeURLPart() + val variableName = variableIndices[i] + if (variableName != null) { + variables[variableName] = urlSegment + score += VARIABLE_MATCH_SCORE + } else if (templateParts[i] == urlSegment) { + score += LITERAL_MATCH_SCORE + } else { + return null + } + } + + return MatchResult(variables = variables.toImmutableMap(), score = score) + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt new file mode 100644 index 000000000..5ba956d29 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt @@ -0,0 +1,67 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate + +/** + * Represents the result of a successful template match. + * + * A higher [score] indicates a more specific match. Implementations must ensure that + * literal segment matches contribute more to the score than variable captures, so that + * a fully literal template (e.g., `users/profile`) always outscores a parameterized + * template (e.g., `users/{id}`) for the same URI. + * + * @property variables A mapping of variable names in the template to their matched values in the URL. + * @property score A non-negative measure of match specificity — higher means more literal segments matched. + */ +public class MatchResult(public val variables: Map, public val score: Int) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is MatchResult) return false + return score == other.score && variables == other.variables + } + + override fun hashCode(): Int = 31 * variables.hashCode() + score + + override fun toString(): String = "MatchResult(variables=$variables, score=$score)" +} + +/** + * Matches resource URIs against a [ResourceTemplate]. + * + * Implementations parse a URI template once at construction time and then match + * candidate URIs against that template via [match]. The returned [MatchResult.score] + * must reflect match specificity so that a selection algorithm can prefer the most + * specific template when multiple templates match the same URI. + */ +public interface ResourceTemplateMatcher { + + public val resourceTemplate: ResourceTemplate + + /** + * Matches a given resource URI against the defined resource template. + * + * @param resourceUri The resource URI to be matched. + * @return A [MatchResult] containing the mapping of variables and a match score + * if the URI matches the template, or null if no match is found. + */ + public fun match(resourceUri: String): MatchResult? +} + +/** + * Factory interface for creating instances of [ResourceTemplateMatcher]. + * + * A [ResourceTemplateMatcher] is used to match resource URIs against a given + * [ResourceTemplate], which adheres to the RFC 6570 URI Template specification. + * This factory abstracts the creation process of a matcher, allowing different + * implementations to define custom matching logic or safeguards (e.g., security + * measures or restrictions on template complexity). + */ +public fun interface ResourceTemplateMatcherFactory { + /** + * Creates a resource template matcher for the given resource template. + * + * @param resourceTemplate The resource template to create a matcher for. + * @return A [ResourceTemplateMatcher] instance that can match URIs against the provided template. + */ + public fun create(resourceTemplate: ResourceTemplate): ResourceTemplateMatcher +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.kt new file mode 100644 index 000000000..452333b86 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.kt @@ -0,0 +1,16 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +/** + * Normalizes [uri] by resolving dot-segments (`/a/b/../c` → `/a/c`) using the + * platform-native URI parser. + * + * On JVM uses [java.net.URI.normalize]. On JS uses the browser/Node.js `URL` API. + * On native and WASM targets [uri] is returned unchanged — no platform normalizer + * is available, so callers on those targets do not receive dot-segment resolution. + * + * Returns [uri] unchanged if it cannot be parsed or normalized. + * + * **Security note**: call this before splitting a URI into segments to prevent + * dot-segment traversal attacks (e.g. `public/../private/secret`). + */ +internal expect fun normalizeUri(uri: String): String diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherSecurityTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherSecurityTest.kt new file mode 100644 index 000000000..fe3fca46d --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherSecurityTest.kt @@ -0,0 +1,221 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +import io.kotest.assertions.withClue +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate +import kotlin.test.Test + +/** + * Security regression tests for [PathSegmentTemplateMatcher]. + * + * Every test **fails** when an attack succeeds — green = defended. + * + * Findings addressed: + * - F1: Exactly one percent-decode pass (no double-decoding). + * - F2: Variable values are attacker-controlled; handlers must validate them. + * - F3: URI segment depth cap prevents allocation amplification. + * - F4: Dot-segment normalization blocks traversal on JVM/JS (no-op on native/WASM). + * - F5: Double-slash — collapsed by JVM normalization or causes size mismatch on native/WASM. + */ +class PathSegmentTemplateMatcherSecurityTest { + + // region: F4: Dot-segment traversal + + /** + * URIs containing `..` or `.` path segments. + * + * On JVM/JS: [normalizeUri] resolves these to their canonical target path, so the + * traversal URI correctly routes to the same template the normalized path would match. + * On native/WASM: [normalizeUri] is a no-op — the extra segments produce a size mismatch + * and the match returns null (different reason, same safety outcome). + */ + private data class TraversalCase(val label: String, val template: String, val uri: String) + + private val traversalNoMatchCases = listOf( + TraversalCase( + label = "single .. traversal to reach private template", + template = "app://host/private/{name}", + uri = "app://host/public/../private/secret", + ), + TraversalCase( + label = "double .. traversal", + template = "app://host/admin/{name}", + uri = "app://host/a/b/../../admin/config", + ), + TraversalCase( + label = ". current-segment in path", + template = "app://host/users/{id}", + uri = "app://host/./users/42", + ), + ) + + @Test + fun `dot-segment traversal resolves to correct target without raw path markers in variables`() { + // On JVM/JS: normalizeUri() resolves `..` and `.` before segment matching. + // → The traversal URI correctly routes to the normalized target template. + // On native/WASM: no normalization — extra segments cause a size mismatch → null. + // In all cases, captured variable values must not contain raw traversal markers. + for ((label, template, uri) in traversalNoMatchCases) { + withClue("traversal '$label': uri='$uri', template='$template'") { + val result = matcher(template).match(uri) + result?.variables?.values?.forEach { value -> + withClue("variable value '$value' must not contain raw '..' or '.' traversal marker") { + value.contains("..") shouldBe false + value shouldNotBe "." + } + } + } + } + } + + @Test + fun `dot-segment URI matches the template it normalizes to`() { + // "public/../users/42" normalizes to "users/42" on JVM/JS, no-op on native/WASM. + // On JVM/JS this must match; the test is platform-conditional via expect/actual. + val result = matcher("users/{id}").match("users/./42") + // After normalization "users/./42" → "users/42" (JVM/JS) or stays 3 segments (native/WASM). + // Either null (native/WASM — 3 segments vs 2) or a valid match (JVM/JS) is acceptable. + // What must NOT happen is a wrong-template match. We check the value if it matches. + result?.variables?.get("id") shouldBe result?.let { "42" } + } + + // endregion + // region: F1: Single percent-decode pass (no double-decoding) + + /** + * %25 is the encoding of '%'. %252F = "%2F" after one decode pass — must NOT become "/". + * If two decode passes were applied, %252F → %2F → /, enabling path traversal. + */ + @Test + fun `double-encoded percent-sign is decoded only once`() { + val result = matcher("files/{path}").match("files/%252Fetc%252Fpasswd") + result.shouldNotBeNull { + // One decode pass: %25 → %, %2F → /... but only %25 is decoded first. + // %252F → %2F (the %25 decodes to %, leaving literal "%2F") + withClue("double-encoded %2F must not become a slash after one decode pass") { + variables["path"]?.contains('/') shouldBe false + } + } + } + + @Test + fun `percent-encoded slash percent-2F in variable is decoded to slash`() { + // %2F IS decoded (one pass) — the result is "/" in the variable value. + // This documents the known behavior: handlers receive decoded values and must + // validate that path separators are not present when constructing file paths. + val result = matcher("files/{path}").match("files/..%2Fetc%2Fpasswd") + result.shouldNotBeNull { + withClue("decoded %2F becomes a path separator — handler must validate") { + variables["path"] shouldBe "../etc/passwd" + } + } + } + + @Test + fun `null byte percent-00 is decoded and passed to handler`() { + // %00 decodes to the null character. Handlers using C-string APIs or file paths + // must reject values containing null bytes. + val result = matcher("items/{id}").match("items/foo%00bar") + result.shouldNotBeNull { + withClue("decoded null byte is present — handler must reject") { + variables["id"] shouldBe "foo\u0000bar" + } + } + } + + @Test + fun `pct-encoded identity bypass`() { + // %69 = 'i'. "adm%69n" decodes to "admin". If a handler compares the raw URI + // against a deny-list of "admin", it must use the decoded value. + val result = matcher("svc/{role}").match("svc/adm%69n") + result.shouldNotBeNull { + variables["role"] shouldBe "admin" + } + } + + // endregion + // region: F3: URI segment depth cap + + @Test + fun `URI exceeding MAX_DEPTH segments returns null`() { + // 51 slash-separated single-char segments → well over the depth cap of 50 + val deepUri = (1..51).joinToString("/") { "x" } + matcher("{a}").match(deepUri) shouldBe null + } + + @Test + fun `URI at exactly MAX_DEPTH segments is not rejected by depth cap`() { + // A URI with exactly 50 segments must be evaluated normally (still non-matching + // against a 1-segment template, but not rejected by the depth guard itself). + val atLimit = (1..50).joinToString("/") { "x" } + // Template has 1 segment, URI has 50 — size mismatch, not depth rejection. + // Both code paths return null; this test verifies no exception is thrown. + matcher("{id}").match(atLimit) shouldBe null + } + + @Test + fun `many-segment URI against many templates completes quickly`() { + val deepUri = (1..49).joinToString("/") { "seg$it" } + val matchers = (1..100).map { matcher("{v$it}") } + // Must not allocate or compute excessively — just verify it finishes. + matchers.forEach { it.match(deepUri) } + } + + // endregion + // region: F5: Double-slash does not produce spurious matches + + @Test + fun `double-slash in URI path is normalized on JVM or causes size mismatch elsewhere`() { + // On JVM: URI("users//42").normalize() collapses "//" to "/" → "users/42" → matches. + // On native/WASM: no normalization — split produces 3 segments vs 2 in template → null. + // Either outcome is safe: no empty-segment injection reaches the handler unchecked. + val result = matcher("users/{id}").match("users//42") + // If matched (JVM), the captured value must be the legitimate path segment. + result?.variables?.get("id") shouldBe result?.let { "42" } + } + + @Test + fun `double-slash in scheme authority path is normalized on JVM or causes size mismatch elsewhere`() { + // On JVM: URI("app://host//injected").normalize() collapses the path "//" to "/" + // → "app://host/injected" → matches "app://host/{id}". + // On native/WASM: no normalization — extra empty segment causes size mismatch → null. + val result = matcher("app://host/{id}").match("app://host//injected") + // If matched (JVM), the captured value must be the legitimate path segment. + result?.variables?.get("id") shouldBe result?.let { "injected" } + } + + // endregion + // region: Query string and fragment pass-through + + @Test + fun `query string and fragment are captured in variable values - handlers must validate`() { + // PathSegmentTemplateMatcher splits on '/' only and does not strip query + // strings or fragments. They are captured verbatim inside the variable value. + // Handlers MUST reject or strip '?' and '#' before using values in file paths, + // database queries, or any other security-sensitive operation. + + // Segment split of "api://host/foo?bar=baz" → ["api:", "", "host", "foo?bar=baz"] (4). + // Template "api://host/{id}" → ["api:", "", "host", "{id}"] (4). + // → Matches on all platforms; query string ends up in the variable value. + val queryResult = matcher("api://host/{id}").match("api://host/foo?bar=baz") + queryResult.shouldNotBeNull { + variables["id"] shouldBe "foo?bar=baz" + } + + // Fragment is also captured verbatim — same reasoning. + val fragmentResult = matcher("api://host/{id}").match("api://host/foo#section") + fragmentResult.shouldNotBeNull { + variables["id"] shouldBe "foo#section" + } + } + + // endregion + // region: Helpers + + private fun matcher(uriTemplate: String): PathSegmentTemplateMatcher = PathSegmentTemplateMatcher( + resourceTemplate = ResourceTemplate(uriTemplate, "Test"), + ) + // endregion +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherTest.kt new file mode 100644 index 000000000..2f43b5e90 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcherTest.kt @@ -0,0 +1,236 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.maps.shouldBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate +import kotlin.test.Test + +class PathSegmentTemplateMatcherTest { + + // region: Construction + + @Test + fun `should throw on blank template`() { + shouldThrow { + matcher(" ") + } + } + + @Test + fun `should throw on empty variable name`() { + shouldThrow { + matcher("users/{}") + } + } + + @Test + fun `should throw when depth exceeds maxTemplateDepth`() { + val deep = "a/b/c/d/e/f/g/h/i/j/k" // 11 segments + shouldThrow { + matcher(deep, maxDepth = 10) + } + } + + @Test + fun `should accept template at exactly maxTemplateDepth`() { + val atLimit = "a/b/c/d/e/f/g/h/i/j" // 10 segments + matcher(atLimit, maxDepth = 10) // must not throw + } + + // endregion + // region: Basic matching + + @Test + fun `should return null for URI with fewer segments than template`() { + matcher("users/{id}/posts").match("users/42") shouldBe null + } + + @Test + fun `should return null for URI with more segments than template`() { + matcher("users/{id}").match("users/42/extra") shouldBe null + } + + @Test + fun `should match all-literal template`() { + val result = matcher("users/profile").match("users/profile") + result.shouldNotBeNull { + variables.shouldBeEmpty() + } + } + + @Test + fun `should return null when literal segment does not match`() { + matcher("users/profile").match("users/settings") shouldBe null + } + + @Test + fun `should extract single variable`() { + val result = matcher("users/{id}").match("users/42") + result.shouldNotBeNull { + variables["id"] shouldBe "42" + } + } + + @Test + fun `should extract multiple variables`() { + val result = matcher("users/{userId}/posts/{postId}").match("users/alice/posts/99") + result.shouldNotBeNull { + variables["userId"] shouldBe "alice" + variables["postId"] shouldBe "99" + } + } + + @Test + fun `should match template with scheme`() { + val result = matcher("test://items/{id}").match("test://items/42") + result.shouldNotBeNull { + variables["id"] shouldBe "42" + } + } + + // endregion + // region:Scoring + + @Test + fun `all-literal template scores higher than parameterized template for same URI`() { + val literal = matcher("users/profile").match("users/profile")!! + val parameterized = matcher("users/{id}").match("users/profile")!! + (literal.score > parameterized.score) shouldBe true + } + + @Test + fun `score increases with number of segments`() { + val short = matcher("a/b").match("a/b")!! + val long = matcher("a/b/c").match("a/b/c")!! + (long.score > short.score) shouldBe true + } + + @Test + fun `all-literal two-segment template score is 4`() { + // 2 literal segments × LITERAL_MATCH_SCORE(2) = 4 + matcher("users/profile").match("users/profile")!!.score shouldBe 4 + } + + @Test + fun `one-variable two-segment template score is 3`() { + // 1 literal × 2 + 1 variable × 1 = 3 + matcher("users/{id}").match("users/42")!!.score shouldBe 3 + } + + @Test + fun `all-variable template scores one per segment`() { + // 2 variables × VARIABLE_MATCH_SCORE(1) = 2 + matcher("{a}/{b}").match("x/y")!!.score shouldBe 2 + } + + // endregion + // region:URL decoding + + @Test + fun `should URL-decode percent-encoded variable value`() { + val result = matcher("search/{query}").match("search/hello%20world") + result.shouldNotBeNull { + variables["query"] shouldBe "hello world" + } + } + + @Test + fun `should URL-decode percent-encoded literal segment before comparing`() { + // %66 decodes to 'f', so "pro%66ile" == "profile" after decoding — it matches + matcher("users/profile").match("users/pro%66ile").shouldNotBeNull() + } + + // endregion + // region:Length guard + + @Test + fun `should return null when URI exceeds maxUrlLength`() { + val longUri = "a/" + "x".repeat(2048) + matcher("a/{id}", maxUrlLength = 2048).match(longUri) shouldBe null + } + + @Test + fun `should match URI at exactly maxUrlLength`() { + // URI of exactly maxUrlLength characters must be accepted + val uri = "a/" + "x".repeat(2046) // length = 2048 + matcher("a/{id}", maxUrlLength = 2048).match(uri).shouldNotBeNull() + } + + // endregion + // region:Edge cases + + @Test + fun `should match root-level single segment template`() { + val result = matcher("{id}").match("42") + result.shouldNotBeNull { + variables["id"] shouldBe "42" + } + } + + @Test + fun `should treat leading and trailing slashes as equivalent`() { + val result = matcher("/users/{id}/").match("/users/7/") + result.shouldNotBeNull { + variables["id"] shouldBe "7" + } + } + + @Test + fun `should capture empty string for single-segment variable when URI is empty`() { + // "".trim('/').split("/") == [""] — one segment — so {id} captures "" + val result = matcher("{id}").match("") + result.shouldNotBeNull { + variables["id"] shouldBe "" + } + } + + @Test + fun `factory creates matcher equal to direct construction`() { + val template = ResourceTemplate("items/{id}", "Items") + val fromFactory = PathSegmentTemplateMatcher.factory.create(template) + val direct = PathSegmentTemplateMatcher(template) + + val uriToMatch = "items/99" + fromFactory.match(uriToMatch) shouldBe direct.match(uriToMatch) + } + + // endregion + // region: MatchResult equality + + @Test + fun `MatchResult equals by value`() { + val a = MatchResult(mapOf("id" to "1"), score = 3) + val b = MatchResult(mapOf("id" to "1"), score = 3) + a shouldBe b + } + + @Test + fun `MatchResult not equal when score differs`() { + val a = MatchResult(mapOf("id" to "1"), score = 3) + val b = MatchResult(mapOf("id" to "1"), score = 2) + (a == b) shouldBe false + } + + @Test + fun `MatchResult not equal when variables differ`() { + val a = MatchResult(mapOf("id" to "1"), score = 3) + val b = MatchResult(mapOf("id" to "2"), score = 3) + (a == b) shouldBe false + } + + // endregion + // region: Helpers + + private fun matcher( + uriTemplate: String, + maxUrlLength: Int = 2048, + maxDepth: Int = 10, + ): PathSegmentTemplateMatcher = PathSegmentTemplateMatcher( + resourceTemplate = ResourceTemplate(uriTemplate, "Test"), + maxUriLength = maxUrlLength, + maxDepth = maxDepth, + ) + // endregion +} diff --git a/kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.js.kt b/kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.js.kt new file mode 100644 index 000000000..fb8aaf450 --- /dev/null +++ b/kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.js.kt @@ -0,0 +1,16 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +private external class URL(url: String) { + val href: String +} + +/** + * Normalizes [uri] via the browser/Node.js `URL` API, resolving dot segments and + * normalizing the scheme, host, and path. Returns [uri] unchanged if it cannot be + * parsed (e.g. relative URIs or schemes unsupported by the runtime). + */ +internal actual fun normalizeUri(uri: String): String = try { + URL(uri).href +} catch (_: Throwable) { + uri +} diff --git a/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.jvm.kt b/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.jvm.kt new file mode 100644 index 000000000..0cde506be --- /dev/null +++ b/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.jvm.kt @@ -0,0 +1,14 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +import java.net.URI +import java.net.URISyntaxException + +/** + * Normalizes [uri] by resolving dot segments (e.g. `/a/b/../c` → `/a/c`) via [URI.normalize]. + * Returns [uri] unchanged if it is not a valid URI. + */ +internal actual fun normalizeUri(uri: String): String = try { + URI(uri).normalize().toString() +} catch (_: URISyntaxException) { + uri +} diff --git a/kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.native.kt b/kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.native.kt new file mode 100644 index 000000000..623fdfc95 --- /dev/null +++ b/kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.native.kt @@ -0,0 +1,3 @@ +package io.modelcontextprotocol.kotlin.sdk.utils +/** No platform-native URI normalizer is available on native targets; [uri] is returned unchanged. */ +internal actual fun normalizeUri(uri: String): String = uri diff --git a/kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.wasmJs.kt b/kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.wasmJs.kt new file mode 100644 index 000000000..7ad7bce9f --- /dev/null +++ b/kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/UriUtils.wasmJs.kt @@ -0,0 +1,4 @@ +package io.modelcontextprotocol.kotlin.sdk.utils + +/** No platform-native URI normalizer is available on WASM targets; [uri] is returned unchanged. */ +internal actual fun normalizeUri(uri: String): String = uri diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index a6f3b6d72..b1b898a33 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -97,6 +97,9 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { 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/Function3;)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/Function3;ILjava/lang/Object;)V + public final fun addResourceTemplate (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate;Lkotlin/jvm/functions/Function4;)V + public final fun addResourceTemplate (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function4;)V + public static synthetic fun addResourceTemplate$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function4;ILjava/lang/Object;)V public final fun addResources (Ljava/util/List;)V public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function3;)V public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function3;)V @@ -113,6 +116,7 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { protected final fun getInstructionsProvider ()Lkotlin/jvm/functions/Function0; protected final fun getOptions ()Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions; public final fun getPrompts ()Ljava/util/Map; + public final fun getResourceTemplates ()Ljava/util/List; public final fun getResources ()Ljava/util/Map; protected final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; public final fun getSessions ()Ljava/util/Map; @@ -128,6 +132,7 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { 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 removeResourceTemplate (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 @@ -141,9 +146,13 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { } public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;)V public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Z)V public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;ZLio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;ZLio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public final fun getResourceTemplateMatcherFactory ()Lio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory; } public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelcontextprotocol/kotlin/sdk/shared/Protocol { diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt index 609aaa234..ce55f3e98 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt @@ -8,7 +8,9 @@ import io.modelcontextprotocol.kotlin.sdk.types.Prompt import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult import io.modelcontextprotocol.kotlin.sdk.types.Resource +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.utils.ResourceTemplateMatcher internal typealias FeatureKey = String @@ -57,3 +59,18 @@ public data class RegisteredResource( ) : Feature { override val key: String = resource.uri } + +/** + * A registered resource template with its associated read handler. + * + * @property resourceTemplate The [ResourceTemplate] definition (RFC 6570 URI template). + * @property readHandler A suspend function invoked when a client reads a URI that matches + * this template. The second parameter contains the URI variables extracted from the match. + */ +internal class RegisteredResourceTemplate( + val resourceTemplate: ResourceTemplate, + val matcher: ResourceTemplateMatcher, + val readHandler: suspend ClientConnection.(ReadResourceRequest, Map) -> ReadResourceResult, +) : Feature { + override val key: String = resourceTemplate.uriTemplate +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index 7842c2b2e..e221482cb 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -25,13 +25,16 @@ import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.McpException import io.modelcontextprotocol.kotlin.sdk.types.Method import io.modelcontextprotocol.kotlin.sdk.types.Notification import io.modelcontextprotocol.kotlin.sdk.types.Prompt import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.RPCError import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult import io.modelcontextprotocol.kotlin.sdk.types.Resource +import io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest @@ -41,9 +44,16 @@ import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations import io.modelcontextprotocol.kotlin.sdk.types.ToolExecution import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.utils.MatchResult +import io.modelcontextprotocol.kotlin.sdk.utils.PathSegmentTemplateMatcher +import io.modelcontextprotocol.kotlin.sdk.utils.ResourceTemplateMatcher +import io.modelcontextprotocol.kotlin.sdk.utils.ResourceTemplateMatcherFactory import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Deferred import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.jvm.JvmOverloads import kotlin.time.ExperimentalTime private val logger = KotlinLogging.logger {} @@ -53,9 +63,20 @@ private val logger = KotlinLogging.logger {} * * @property capabilities The capabilities this server supports. * @property enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with clients. + * @property resourceTemplateMatcherFactory The factory used to create [ResourceTemplateMatcher] instances + * for matching resource URIs against registered templates. Defaults to [PathSegmentTemplateMatcher.factory]. */ -public class ServerOptions(public val capabilities: ServerCapabilities, enforceStrictCapabilities: Boolean = true) : - ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities) +public class ServerOptions( + public val capabilities: ServerCapabilities, + enforceStrictCapabilities: Boolean = true, + public val resourceTemplateMatcherFactory: ResourceTemplateMatcherFactory = PathSegmentTemplateMatcher.factory, +) : ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities) { + @JvmOverloads + public constructor( + capabilities: ServerCapabilities, + enforceStrictCapabilities: Boolean = true, + ) : this(capabilities, enforceStrictCapabilities, PathSegmentTemplateMatcher.factory) +} /** * An MCP server is responsible for storing features and handling new connections. @@ -126,6 +147,11 @@ public open class Server( addListener(notificationService.resourceUpdatedListener) } } + private val resourceTemplateRegistry = FeatureRegistry("ResourceTemplate").apply { + if (options.capabilities.resources?.listChanged == true) { + addListener(notificationService.resourceListChangedListener) + } + } /** * Provides a snapshot of all sessions currently registered in the server @@ -151,6 +177,12 @@ public open class Server( public val resources: Map get() = resourceRegistry.values + /** + * Provides a snapshot of all resource templates currently registered in the server. + */ + public val resourceTemplates: List + get() = resourceTemplateRegistry.values.values.map { it.resourceTemplate } + init { block(this) } @@ -297,7 +329,7 @@ public open class Server( * @throws IllegalStateException If the server does not support tools. */ public fun addTool(tool: Tool, handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult) { - check(options.capabilities.tools != null) { + checkNotNull(options.capabilities.tools) { logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" } "Server does not support tools capability. Enable it in ServerOptions." } @@ -351,7 +383,7 @@ public open class Server( * @throws IllegalStateException If the server does not support tools. */ public fun addTools(toolsToAdd: List) { - check(options.capabilities.tools != null) { + checkNotNull(options.capabilities.tools) { logger.error { "Failed to add tools: Server does not support tools capability" } "Server does not support tools capability." } @@ -366,7 +398,7 @@ public open class Server( * @throws IllegalStateException If the server does not support tools. */ public fun removeTool(name: String): Boolean { - check(options.capabilities.tools != null) { + checkNotNull(options.capabilities.tools) { logger.error { "Failed to remove tool '$name': Server does not support tools capability" } "Server does not support tools capability." } @@ -381,7 +413,7 @@ public open class Server( * @throws IllegalStateException If the server does not support tools. */ public fun removeTools(toolNames: List): Int { - check(options.capabilities.tools != null) { + checkNotNull(options.capabilities.tools) { logger.error { "Failed to remove tools: Server does not support tools capability" } "Server does not support tools capability." } @@ -401,7 +433,7 @@ public open class Server( prompt: Prompt, promptProvider: suspend ClientConnection.(GetPromptRequest) -> GetPromptResult, ) { - check(options.capabilities.prompts != null) { + checkNotNull(options.capabilities.prompts) { logger.error { "Failed to add prompt '${prompt.name}': Server does not support prompts capability" } "Server does not support prompts capability." } @@ -434,7 +466,7 @@ public open class Server( * @throws IllegalStateException If the server does not support prompts. */ public fun addPrompts(promptsToAdd: List) { - check(options.capabilities.prompts != null) { + checkNotNull(options.capabilities.prompts) { logger.error { "Failed to add prompts: Server does not support prompts capability" } "Server does not support prompts capability." } @@ -449,7 +481,7 @@ public open class Server( * @throws IllegalStateException If the server does not support prompts. */ public fun removePrompt(name: String): Boolean { - check(options.capabilities.prompts != null) { + checkNotNull(options.capabilities.prompts) { logger.error { "Failed to remove prompt '$name': Server does not support prompts capability" } "Server does not support prompts capability." } @@ -465,7 +497,7 @@ public open class Server( * @throws IllegalStateException If the server does not support prompts. */ public fun removePrompts(promptNames: List): Int { - check(options.capabilities.prompts != null) { + checkNotNull(options.capabilities.prompts) { logger.error { "Failed to remove prompts: Server does not support prompts capability" } "Server does not support prompts capability." } @@ -490,7 +522,7 @@ public open class Server( mimeType: String = "text/html", readHandler: suspend ClientConnection.(ReadResourceRequest) -> ReadResourceResult, ) { - check(options.capabilities.resources != null) { + checkNotNull(options.capabilities.resources) { logger.error { "Failed to add resource '$name': Server does not support resources capability" } "Server does not support resources capability." } @@ -505,7 +537,7 @@ public open class Server( * @throws IllegalStateException If the server does not support resources. */ public fun addResources(resourcesToAdd: List) { - check(options.capabilities.resources != null) { + checkNotNull(options.capabilities.resources) { logger.error { "Failed to add resources: Server does not support resources capability" } "Server does not support resources capability." } @@ -520,7 +552,7 @@ public open class Server( * @throws IllegalStateException If the server does not support resources. */ public fun removeResource(uri: String): Boolean { - check(options.capabilities.resources != null) { + checkNotNull(options.capabilities.resources) { logger.error { "Failed to remove resource '$uri': Server does not support resources capability" } "Server does not support resources capability." } @@ -535,13 +567,74 @@ public open class Server( * @throws IllegalStateException If the server does not support resources. */ public fun removeResources(uris: List): Int { - check(options.capabilities.resources != null) { + checkNotNull(options.capabilities.resources) { logger.error { "Failed to remove resources: Server does not support resources capability" } "Server does not support resources capability." } return resourceRegistry.removeAll(uris) } + /** + * Registers a resource template. Clients can discover it via `resources/templates/list` + * and read matching URIs via `resources/read`. + * + * @param template The [ResourceTemplate] describing the URI template pattern. + * @param readHandler A suspend function invoked when a client reads a URI that matches + * the template. The second parameter contains the URI variables extracted from the match. + * @throws IllegalStateException If the server does not support resources. + */ + public fun addResourceTemplate( + template: ResourceTemplate, + readHandler: suspend ClientConnection.(ReadResourceRequest, Map) -> ReadResourceResult, + ) { + checkNotNull(options.capabilities.resources) { + "Server does not support resources capability." + } + val matcher = options.resourceTemplateMatcherFactory.create(template) + resourceTemplateRegistry.add( + RegisteredResourceTemplate( + template, + matcher, + readHandler = readHandler, + ), + ) + } + + /** + * Registers a resource template by constructing a [ResourceTemplate] from given parameters. + * + * @param uriTemplate The RFC 6570 URI template string (e.g. `"file:///{path}"`). + * @param name A human-readable name for the template. + * @param description A human-readable description of the resource template. + * @param mimeType The MIME type of resource content served by this template. + * @param readHandler A suspend function invoked when a client reads a URI that matches + * the template. The second parameter contains the URI variables extracted from the match. + * @throws IllegalStateException If the server does not support resources. + */ + public fun addResourceTemplate( + uriTemplate: String, + name: String, + description: String? = null, + mimeType: String? = null, + readHandler: suspend ClientConnection.(ReadResourceRequest, Map) -> ReadResourceResult, + ) { + addResourceTemplate(ResourceTemplate(uriTemplate, name, description, mimeType), readHandler) + } + + /** + * Removes a resource template by its URI template string. + * + * @param uriTemplate The URI template string identifying the template to remove. + * @return True if the template was removed, false if it was not found. + * @throws IllegalStateException If the server does not support resources. + */ + public fun removeResourceTemplate(uriTemplate: String): Boolean { + checkNotNull(options.capabilities.resources) { + "Server does not support resources capability." + } + return resourceTemplateRegistry.remove(uriTemplate) + } + // --- Internal Handlers --- private fun handleSubscribeResources(session: ServerSession, request: SubscribeRequest) { if (options.capabilities.resources?.subscribe ?: false) { @@ -621,21 +714,38 @@ public open class Server( } private suspend fun handleReadResource(session: ServerSession, request: ReadResourceRequest): ReadResourceResult { - val requestParams = request.params - logger.debug { "Handling read resource request for: ${requestParams.uri}" } - val resource = resourceRegistry.get(requestParams.uri) + val uri = request.params.uri + logger.debug { "Handling read resource request for: $uri" } + + // Priority 1: exact URI match + resourceRegistry.get(uri)?.let { resource -> + return resource.run { session.clientConnection.readHandler(request) } + } + + // Priority 2: most-specific matching template. + // Selection: highest score wins; on tie, fewest variables wins; on tie, registration order wins. + val (template, matchResult) = resourceTemplateRegistry.values.values + .mapNotNull { tmpl -> tmpl.matcher.match(uri)?.let { tmpl to it } } + .maxWithOrNull( + compareBy> { (_, result) -> result.score } + .thenByDescending { (_, result) -> result.variables.size }, + ) ?: run { - logger.error { "Resource not found: ${requestParams.uri}" } - throw IllegalArgumentException("Resource not found: ${requestParams.uri}") + logger.error { "Resource not found: $uri" } + throw McpException( + code = RPCError.ErrorCode.RESOURCE_NOT_FOUND, + message = "Resource not found", + data = buildJsonObject { put("uri", uri) }, + ) } - return resource.run { - session.clientConnection.readHandler(request) - } + + logger.debug { "Matched resource template '${template.key}' for URI: $uri" } + return template.run { session.clientConnection.readHandler(request, matchResult.variables) } } private fun handleListResourceTemplates(): ListResourceTemplatesResult { - // If you have resource templates, return them here. For now, return empty. - return ListResourceTemplatesResult(listOf()) + logger.debug { "Handling list resource templates request" } + return ListResourceTemplatesResult(resourceTemplateRegistry.values.values.map { it.resourceTemplate }) } // Start the ServerSession / ClientConnection redirection section From 62e4e50c676405214bb238f10250f2d20b812444 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Fri, 20 Mar 2026 15:01:58 +0100 Subject: [PATCH 029/187] docs: update AGENTS.md (#612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve AGENTS.md ## Motivation and Context The previous AGENTS.md contained many elements contradicting [best practices](https://code.claude.com/docs/en/best-practices). AGENTS.md/CLAUDE.md is loaded into the context on every request, so a ~300+ line file is too large and fills the context window. > Keep it concise. For each line, ask: “Would removing this cause Claude to make mistakes?” If not, cut it. Bloated CLAUDE.md files cause Claude to ignore your actual instructions! > Most best practices are based on one constraint: Claude’s context window fills up fast, and performance degrades as it fills. > The over-specified CLAUDE.md. If your CLAUDE.md is too long, Claude ignores half of it because important rules get lost in the noise. Since the file is loaded on every request, it should be general-purpose rather than targeting specific use cases. > CLAUDE.md is loaded every session, so only include things that apply broadly. AI coding agents are used not only for writing tests and implementations. - AGENTS.md should not contain behavioral instructions, especially like `Tests first, always.`. This causes the agent to trigger test runs even when not required, increasing request time and token consumption significantly due to large test outputs. For example, on a simple codebase question, the agent ran the full test suite and I waited over 10 minutes for a response. - `Uphold SOLID principles` — self-evident rules, explaining what SOLID stands for is redundant since AI coding agents are already trained on this knowledge. > Exclude: "Standard language conventions Claude already knows" > Exclude: "Self-evident practices like 'write clean code'" - Mentioning specific MCP servers (jetbrains, intellij-index) can mislead the agent if they are not installed in the environment. Additionally, [Anthropic recommends](https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf) mentioning the use of MCP in skills. - The large Kotest patterns block targets a specific use case and is unnecessary for other agent interactions. Moreover, the agent already looks at neighboring test classes and can learn patterns from existing tests - The large Module documentation block covers a very rare use case. - The KDoc block — the agent can derive this information from the codebase, this should be a skill instead These and other changes follow the best practices. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context https://code.claude.com/docs/en/best-practices https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf https://agents.md/#examples --- AGENTS.md | 370 +++++++++--------------------------------------------- CLAUDE.md | 2 +- 2 files changed, 60 insertions(+), 312 deletions(-) mode change 100644 => 120000 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 1646c55a4..6ef38be6b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,334 +1,82 @@ # AGENTS: Development Guidelines for AI Contributors -This document is for autonomous agents and AI copilots contributing code to this repository. -Follow these rules to keep changes safe, comprehensible, and easy to maintain. +This file provides guidance to AI coding agents working with code in this repository. -## Repository Layout +## Project -- `kotlin-sdk-core`: Core protocol types, transport abstractions, serialization utilities -- `kotlin-sdk-client`: Client transports (STDIO, SSE, StreamableHttp, WebSocket) -- `kotlin-sdk-server`: Server transports + Ktor integration (STDIO, SSE, WebSocket) -- `kotlin-sdk`: Umbrella module that aggregates all three above -- `test-utils`: Shared test utilities -- `integration-test`: Integration tests with TypeScript/other implementations -- `conformance-test`: Protocol conformance tests -- `samples/`: Sample projects (composite builds) -- Package structure: `io.modelcontextprotocol.kotlin.sdk.*` -- JVM 11+, Kotlin 2.2+ +Kotlin Multiplatform SDK for the [Model Context Protocol (MCP)](https://modelcontextprotocol.io). Targets JVM, Native (macOS/Linux/Windows), JS, and Wasm. Implements both MCP clients and servers with pluggable transports. -## Prime Directives +## Build Commands -1. **Tests first, always.** - - Before changing code, identify or add tests that express the desired behavior. - - Prefer readable, minimal tests over clever ones. Tests are documentation. -2. **Keep tests simple and explicit.** - - Arrange/Act/Assert structure; avoid hidden magic and overuse of helpers. - - Prefer concrete inputs/outputs; avoid randomness and time dependence. -3. **Uphold SOLID principles** in production code: - - Single Responsibility: each class/function should do one thing well. - - Open/Closed: extend via new code, avoid risky edits to stable code paths. - - Liskov Substitution: honor contracts; keep types substitutable. - - Interface Segregation: keep abstractions small and focused. - - Dependency Inversion: depend on abstractions, not concretions. -4. **Make the minimal change** that satisfies the tests and the issue. - Stay focused on a single concern and avoid "drive-by" changes. -5. **Never commit changes!** It's human who should verify and commit to git. -6. **Keep the build green.** Do not merge changes that break existing tests. -7. **Prefer clarity** over micro-optimizations and cleverness. -8. **Ask when uncertain.** If requirements are ambiguous, request clarification with a concise question. -9. **Write code with the quality of a Kotlin Champion.** -10. **Use MCP servers** like `jetbrains` and `intellij-index` to explore the codebase, for code navigation, analysis, - refactoring, and running tests. -11. **Use the Bash tool for terminal commands** instead of MCP terminal execution features. -12. **Suggest updates** to AGENTS.md/CLAUDE.md with best practices and guidelines. - -## Code Style - -### Kotlin - -- Avoid changing public API signatures. Run `./gradlew apiCheck` before every commit. -- **Explicit API mode** is strict: all public APIs must have explicit visibility modifiers and return types. -- Anything under an `internal` directory is not part of the public API and may change freely. -- Package structure follows: `io.modelcontextprotocol.kotlin.sdk.*` -- Follow Kotlin coding conventions -- Use the provided `.editorconfig` for consistent formatting -- Use Kotlin typesafe DSL builders where possible; prioritize fluent builder style over standard builder methods -- Prefer DSL builder style (method with lambda blocks) over constructors, if possible -- Use `val` for immutable properties and `var` for mutable; consider `lateinit var` instead of nullable types when - appropriate -- Use multi-dollar interpolation prefix for strings where applicable -- Use fully qualified imports instead of star imports -- Ensure backward compatibility when making changes - -## Testing Guidance - -### Test Organization - -- **Location**: `src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/` -- **Platform-specific**: `src/jvmTest/`, `src/jsTest/`, etc. -- **Naming**: Use function `Names with backticks`, e.g., `fun \`should return 200 OK\`()` -- **Structure**: Arrange/Act/Assert pattern -- **Documentation**: Keep tests self-documenting; don't add KDocs to tests. - -### Test Principles - -- Write comprehensive tests for new features. Always add tests for new features or bug fixes, even if not explicitly - requested. -- **Prioritize test readability** -- Avoid creating too many test methods; use parametrized tests when testing multiple similar scenarios -- When running tests on Kotlin Multiplatform projects, run JVM tests only unless asked for other platforms - -### Test Framework Stack - -- **kotlin-test**: Core test framework -- **Kotest assertions**: Use infix style (`shouldBe`, `shouldContain`) instead of `assertEquals`. - - Use `shouldMatchJson` from kotest-assertions-json for JSON validation. - - For nullable objects with nested properties, prefer `shouldNotBeNull { ... }` blocks: - ```kotlin - content.annotations shouldNotBeNull { - priority shouldBe 1.0 - } - ``` -- **mockk**: Mocking library -- **Ktor MockEngine**: For HTTP client mocking (`io.ktor:ktor-client-mock`) -- **Java tests**: Use JUnit5, Mockito, AssertJ core -- **Serialization test utilities** (`io.modelcontextprotocol.kotlin.test.utils`): - - `verifySerialization(value, json, expectedJson)` — serializes, asserts match, round-trips back; use for most serialization tests - - `verifyDeserialization(json, payload)` — deserializes from JSON, re-serializes, asserts match; returns the object for further assertions - - Always test both empty/null/omitted and non-null cases for nullable fields; `McpJson` has `explicitNulls = false` so null properties must be absent from JSON, not `null` - -### Kotest Patterns - -- Basic assertions: Use infix style (`shouldBe`, `shouldContain`) instead of `assertEquals`. - ```kotlin - result shouldBe expected - list shouldContain item - ``` -- Nullable Assertions - ```kotlin - // Preferred: lambda-style for multiple assertions - content.annotations shouldNotBeNull { - priority shouldBe 1.0 - } - ``` - -- JSON Assertions (kotest-assertions-json): - ```kotlin - // language=json - val expected = """ - { - "id": 123 - } - """.trimIndent() - actual shouldEqualJson expected - ``` - -- Soft Assertions - ```kotlin - // Use for multiple assertions on SAME subject - assertSoftly(subject) { - name shouldBe "test" - value shouldBe 42 - } - // DON'T use for different subjects or single assertions - ``` -- Clues (Use Sparingly) - ```kotlin - // Only when assertion is NOT obvious - withClue("Failed to parse response") { - result shouldNotBe null - } - ``` - -#### Avoiding `this.` in Lambda Blocks - -```kotlin -// Avoid explicit `this.` unless necessary for disambiguation -prop.shouldNotBeNull { - enum shouldHaveSize 2 // Preferred - this.enum shouldHaveSize 2 // Avoid -} +```bash +./gradlew ktlintCheck # Lint check +./gradlew detekt # Static analysis +./gradlew ktlintFormat # Auto-fix lint issues +./gradlew apiCheck # Check public API compatibility (run before committing) +./gradlew apiDump # Update API dump after intentional API changes +./gradlew koverLog # Print coverage summary +./gradlew koverXmlReport # Generate XML coverage report ``` -### Multiplatform Patterns - -- Use `expect`/`actual` pattern for platform-specific implementations in `utils.*` files. -- Test changes on JVM first, then verify platform-specific behavior if needed. -- Supported targets: JVM (11+), JS/Wasm, iOS, watchOS, tvOS. - -## Coding Guidance - -### Module Boundaries - -- Respect separation of concerns between modules -- Keep abstractions framework-agnostic -- Avoid leaking implementation details across module boundaries - -### Serialization - -- Use Kotlinx Serialization with explicit `@Serializable` annotations -- JSON config is defined in `jsonUtils.kt` as `McpJson` — use it consistently -- Register custom serializers in companion objects - -### Error Handling - -- Use sealed classes for result states -- Map errors to JSON-RPC error codes in protocol handlers -- Log errors using `io.github.oshai.kotlinlogging.KotlinLogging` -- Never log sensitive data (credentials, tokens) - -### Code Quality - -- Handle nullability and references as established patterns -- Keep changes small and reversible; prefer adding functions over editing many call sites -- Add focused comments **only** where intent is non-obvious -- Use `git mv` command when moving version-controlled files -- Never commit or push changes. This is a Human responsibility. - -## Workflow for AI Agents - -1. **Understand the issue** - - Summarize requirement in 1-2 sentences - - Identify affected files and tests; ask if unclear - -2. **Plan minimal changes** - - Use TaskCreate/TaskUpdate to track work, or EnterPlanMode for complex changes - - Document affected files and approach - -3. **Work with code** - - View, edit, and run tests through MCP tools - -4. **Start with tests** - - Update existing or add new tests in relevant modules - -5. **Implement the change** - - Prefer composition and small functions - - Avoid breaking public APIs without tests and discussion - -6. **Run tests locally** - ```bash - ./gradlew test - ./gradlew :kotlin-sdk-core:test # Specific module - ./gradlew :integration-test:test # Integration tests - ``` - -7. **Verify results** - - Use Kotest JSON matchers for schema validation - - Double-check the readability of code and tests - -8. **Document key decisions** - - Brief notes in markdown document, if requested - -## Build and Run Commands - +Run a single test class: ```bash -# Build all modules -./gradlew build - -# Run all tests -./gradlew test - -# Run specific module tests -./gradlew :kotlin-sdk-core:test -./gradlew :kotlin-sdk-client:test -./gradlew :kotlin-sdk-server:test - -# Check API compatibility (required before commit) -./gradlew apiCheck - -# Update API dump (after intentional API changes) -./gradlew apiDump - -# Platform-specific tests -./gradlew jsTest -./gradlew wasmJsTest +./gradlew :kotlin-sdk-server:jvmTest --tests "io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransportTest" ``` -## Definition of done - -- Tests for affected functionality exist -- All tests pass across all targets -- Changes are focused on a single concern. -- Code respects module boundaries -- `./gradlew apiCheck` passes -- Documentation updated for user-facing changes - -## Documentation - -### General Guidelines - -- **Never make up documentation/facts** -- Update README files when adding new features -- Document API changes in the appropriate module -- **Keep documentation concise and straight to the point** -- Always verify documentation matches code; fix discrepancies -- Write tutorials in Markdown format in README.md +Run a single test method: +```bash +./gradlew :kotlin-sdk-core:jvmTest --tests "*.ProtocolTest.should preserve existing meta when adding progress token" +``` -### KDoc Guidelines +Conformance tests (requires Node.js + npx): +```bash +./conformance-test/run-conformance.sh all # All suites sequentially +./conformance-test/run-conformance.sh server # Server suite only +./conformance-test/run-conformance.sh client # Client core suite (initialize, tools_call, elicitation, sse-retry) +./conformance-test/run-conformance.sh client-auth # Client auth suite (20 OAuth scenarios) +``` -- Properly document interfaces and abstract classes in production code -- Avoid KDocs on override functions to reduce verbosity -- Update KDocs when API changes -- Use references: `[SendMessageRequest]` instead of `SendMessageRequest` -- Add brief code examples to KDocs -- Add links to specifications with verified URLs; never add broken links +## Module Structure -### Module.md Files for Dokka +``` +kotlin-sdk-core Core: protocol types, JSON-RPC, Transport abstraction, Protocol base class +kotlin-sdk-client Client: MCP client implementation (tools, prompts, resources, sampling) +kotlin-sdk-server Server: MCP server implementation (feature registries, sessions) +kotlin-sdk-testing ChannelTransport for in-memory client-server testing +kotlin-sdk Umbrella module aggregating client + server +test-utils Shared test helpers (mockk, kotest, awaitility) +conformance-test MCP specification compliance tests (external runner) +integration-test End-to-end tests +samples/ Runnable examples (servers and clients) +``` -Each module must have a `Module.md` file at the module root for Dokka-generated API documentation. +## Architecture -**Format Requirements** (per [Dokka specification](https://kotlinlang.org/docs/dokka-module-and-package-docs.html)): +Layered design: -- Must start with `# Module module-name` (level 1 header) -- Package documentation uses `# Package package.name` (level 1 header) -- All other content uses level 2+ headers (`##`, `###`, etc.) +``` +Application (tools, prompts, resources) + → Client / Server (typed MCP operations, capability enforcement) + → Protocol (JSON-RPC framing, request correlation, timeouts) + → Transport (pluggable I/O: stdio, SSE, Streamable HTTP, WebSocket) +``` -**Content Guidelines**: +## Code Conventions -- **Purpose**: Provide module-level overview in generated API docs -- **Detail level**: API documentation style (concise, focused on classes/interfaces) -- **Module section**: - - Brief module description (1-2 sentences) - - Platform support (format: "Multiplatform • Kotlin 2.2+" or "JVM only • Kotlin 2.2+") - - Key classes/components (use Dokka references like `[ClassName]`) - - Example (minimal, demonstrating primary usage) -- **Package sections**: Use `# Package package.name` headers with brief description -- **Optional sections**: Features, Limitations, Related Specifications -- **Style**: - - Use bullet points over prose - - Keep examples short (5-15 lines) - - Reference related modules using Dokka links - - Avoid duplicating README content +- **Explicit API mode (Strict)**: all public declarations must have explicit visibility modifiers and return types +- **DSL builders**: prefer DSL builder style (lambda blocks) over direct constructors +- **Multi-dollar interpolation**: use `$$"""..."""` for strings containing literal `$` (e.g., JSON with `$ref`, `$defs`) +- **`McpJson` has `explicitNulls = false`**: null properties must be absent from JSON, not `"field": null` +- **Logging**: use `io.github.oshai.kotlinlogging.KotlinLogging` +- **Structured concurrency**: every coroutine must have a bounded scope and lifetime; always re-throw `CancellationException`; never hold `Mutex` across suspension points -**Example structure (markdown)**: +## Testing - # Module module-name - - Brief description. - - **Platform Support:** Multiplatform • Kotlin 2.2+ - - ## Key Classes - - - [MainClass] - what it does - - [HelperClass] - what it does - - ## Example - - ```kotlin - val example = MainClass() - ``` - - # Package package.name - - Package description. - - # Package another.package - - Another package description. +- **JVM first**: run `jvmTest` instead of `test` — full multiplatform test runs are slow and rarely needed during development +- **Test naming**: use backtick syntax — `fun \`should return 200 OK\`()` +- **Frameworks**: JUnit 5 (runner) + Kotest (assertions) + MockK (mocking) + Awaitility (async waits) +- **In-memory testing**: `ChannelTransport.createLinkedPair()` connects client and server without network -## When to Ask for Help +## Skills -- Requirements conflict with existing tests or documentation -- The smallest change still requires altering core abstractions -- Unclear expected schema shape for a new feature—ask for a concrete, readable test case to anchor implementation +- `/mcp-docs` — fetches live MCP specification and docs; use when implementing or debugging MCP protocol features diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 43c994c2d..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@AGENTS.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From fd3a8583b848c3ccadfd66c7b187e3aa86a5152d Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Fri, 20 Mar 2026 18:07:32 +0100 Subject: [PATCH 030/187] feat: add tasks models (#566) ## Motivation and Context closes #421 #406 ## How Has This Been Tested? Unit tests ## Breaking Changes NaN ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- kotlin-sdk-core/api/kotlin-sdk-core.api | 564 ++++++++++++ .../kotlin/sdk/types/methods.kt | 5 + .../kotlin/sdk/types/notification.kt | 45 + .../kotlin/sdk/types/request.kt | 12 + .../kotlin/sdk/types/serializers.kt | 17 + .../kotlin/sdk/types/tasks.kt | 347 ++++++++ .../kotlin/sdk/types/NotificationTest.kt | 112 +++ .../kotlin/sdk/types/TasksTest.kt | 802 ++++++++++++++++++ 8 files changed, 1904 insertions(+) create mode 100644 kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt create mode 100644 kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/TasksTest.kt diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index dc3da3e44..7e1b98c50 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -848,6 +848,67 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Compa public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest;Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequest; + 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/types/CancelTaskRequest;)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/types/CancelTaskRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-D-90kyg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams; + public static synthetic fun copy-D-90kyg$default (Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelTaskRequestParams; + 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/types/CancelTaskRequestParams;)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/types/CancelTaskRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotification : io/modelcontextprotocol/kotlin/sdk/types/ClientNotification, io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotification$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams;)V @@ -1403,6 +1464,37 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult$ public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Task;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Task;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Task; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Task;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult;Lio/modelcontextprotocol/kotlin/sdk/types/Task;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/Task; + 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/types/CreateTaskResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult; + 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/types/CreateTaskResult;)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/types/CreateTaskResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/CustomNotification : io/modelcontextprotocol/kotlin/sdk/types/ClientNotification, io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CustomNotification$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)V @@ -1798,6 +1890,206 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptResult$Comp public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest;Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequest; + 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/types/GetTaskPayloadRequest;)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/types/GetTaskPayloadRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-D-90kyg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams; + public static synthetic fun copy-D-90kyg$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadRequestParams; + 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/types/GetTaskPayloadRequestParams;)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/types/GetTaskPayloadRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult$Companion; + public static final synthetic fun box-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult; + public static fun constructor-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z + public static final fun get-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;)Lkotlinx/serialization/json/JsonElement; + public final fun getJson ()Lkotlinx/serialization/json/JsonObject; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public static fun getMeta-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject; + public fun get_meta ()Lkotlinx/serialization/json/JsonObject; + public static fun get_meta-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Lkotlinx/serialization/json/JsonObject; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize-iveUaak (Lkotlinx/serialization/encoding/Decoder;)Lkotlinx/serialization/json/JsonObject; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize-kradpfw (Lkotlinx/serialization/encoding/Encoder;Lkotlinx/serialization/json/JsonObject;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest;Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequest; + 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/types/GetTaskRequest;)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/types/GetTaskRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams$Companion; + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-D-90kyg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams; + public static synthetic fun copy-D-90kyg$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskRequestParams; + 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/types/GetTaskRequestParams;)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/types/GetTaskRequestParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult, io/modelcontextprotocol/kotlin/sdk/types/TaskFields { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult$Companion; + public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/Long; + public final fun component7 ()Ljava/lang/Long; + public final fun component8 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult; + public fun equals (Ljava/lang/Object;)Z + public fun getCreatedAt ()Ljava/lang/String; + public fun getLastUpdatedAt ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getPollInterval ()Ljava/lang/Long; + public fun getStatus ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public fun getStatusMessage ()Ljava/lang/String; + public fun getTaskId ()Ljava/lang/String; + public fun getTtl ()Ljava/lang/Long; + 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/types/GetTaskResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/GetTaskResult; + 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/types/GetTaskResult;)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/types/GetTaskResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/Icon { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Companion; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/Icon$Theme;)V @@ -2548,6 +2840,72 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsResult$Comp public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest, io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest;Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest; + public fun equals (Ljava/lang/Object;)Z + public fun getCursor ()Ljava/lang/String; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksRequest; + 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/types/ListTasksRequest;)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/types/ListTasksRequest$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ListTasksResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult, io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult$Companion; + public fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult;Ljava/util/List;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult; + public fun equals (Ljava/lang/Object;)Z + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getNextCursor ()Ljava/lang/String; + public final fun getTasks ()Ljava/util/List; + 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/types/ListTasksResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ListTasksResult; + 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/types/ListTasksResult;)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/types/ListTasksResult$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest$Companion; public fun ()V @@ -2797,6 +3155,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Defined : jav public static final field NotificationsResourcesListChanged Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field NotificationsResourcesUpdated Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field NotificationsRootsListChanged Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsTasksStatus Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field NotificationsToolsListChanged Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field Ping Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field PromptsGet Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; @@ -2808,6 +3167,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Defined : jav public static final field ResourcesUnsubscribe Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field RootsList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field SamplingCreateMessage Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field TasksCancel Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field TasksGet Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field TasksList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field TasksResult Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field ToolsCall Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field ToolsList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static fun getEntries ()Lkotlin/enums/EnumEntries; @@ -3439,6 +3802,33 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ReferenceType$Compan public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata$Companion; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata; + public fun equals (Ljava/lang/Object;)Z + public final fun getTaskId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata; + 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/types/RelatedTaskMetadata;)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/types/RelatedTaskMetadata$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/Request { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Request$Companion; public abstract fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; @@ -3534,6 +3924,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RequestMeta { public static final fun get-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;)Lkotlinx/serialization/json/JsonElement; public final fun getJson ()Lkotlinx/serialization/json/JsonObject; public static final fun getProgressToken-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public static final fun getRelatedTask-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata; public fun hashCode ()I public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I public fun toString ()Ljava/lang/String; @@ -4337,6 +4728,175 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestPara public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/Task : io/modelcontextprotocol/kotlin/sdk/types/TaskFields { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Task$Companion; + public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/Long; + public final fun component7 ()Ljava/lang/Long; + public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;)Lio/modelcontextprotocol/kotlin/sdk/types/Task; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/Task;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Task; + public fun equals (Ljava/lang/Object;)Z + public fun getCreatedAt ()Ljava/lang/String; + public fun getLastUpdatedAt ()Ljava/lang/String; + public fun getPollInterval ()Ljava/lang/Long; + public fun getStatus ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public fun getStatusMessage ()Ljava/lang/String; + public fun getTaskId ()Ljava/lang/String; + public fun getTtl ()Ljava/lang/Long; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/Task$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/Task$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/Task; + 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/types/Task;)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/types/Task$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/TaskFields { + public abstract fun getCreatedAt ()Ljava/lang/String; + public abstract fun getLastUpdatedAt ()Ljava/lang/String; + public abstract fun getPollInterval ()Ljava/lang/Long; + public abstract fun getStatus ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public abstract fun getStatusMessage ()Ljava/lang/String; + public abstract fun getTaskId ()Ljava/lang/String; + public abstract fun getTtl ()Ljava/lang/Long; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskMetadata { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata$Companion; + public fun ()V + public fun (Ljava/lang/Long;)V + public synthetic fun (Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Long; + public final fun copy (Ljava/lang/Long;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Ljava/lang/Long;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public fun equals (Ljava/lang/Object;)Z + public final fun getTtl ()Ljava/lang/Long; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TaskMetadata$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + 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/types/TaskMetadata;)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/types/TaskMetadata$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskStatus : java/lang/Enum { + public static final field Cancelled Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus$Companion; + public static final field Completed Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static final field Failed Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static final field InputRequired Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static final field Working Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskStatus$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification : io/modelcontextprotocol/kotlin/sdk/types/ClientNotification, io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotification; + 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/types/TaskStatusNotification;)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/types/TaskStatusNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams : io/modelcontextprotocol/kotlin/sdk/types/NotificationParams, io/modelcontextprotocol/kotlin/sdk/types/TaskFields { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams$Companion; + public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/Long; + public final fun component7 ()Ljava/lang/Long; + public final fun component8 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Long;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams; + public fun equals (Ljava/lang/Object;)Z + public fun getCreatedAt ()Ljava/lang/String; + public fun getLastUpdatedAt ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun getPollInterval ()Ljava/lang/Long; + public fun getStatus ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatus; + public fun getStatusMessage ()Ljava/lang/String; + public fun getTaskId ()Ljava/lang/String; + public fun getTtl ()Ljava/lang/Long; + 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/types/TaskStatusNotificationParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificationParams; + 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/types/TaskStatusNotificationParams;)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/types/TaskStatusNotificationParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/TaskSupport : java/lang/Enum { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport$Companion; public static final field Forbidden Lio/modelcontextprotocol/kotlin/sdk/types/TaskSupport; @@ -4351,6 +4911,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/TaskSupport$Companio public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/TasksKt { + public static final field RELATED_TASK_META_KEY Ljava/lang/String; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/TextContent : io/modelcontextprotocol/kotlin/sdk/types/MediaContent { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TextContent$Companion; public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)V diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt index 997001569..1d96fea03 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt @@ -32,6 +32,7 @@ public sealed interface Method { NotificationsToolsListChanged("notifications/tools/list_changed"), NotificationsRootsListChanged("notifications/roots/list_changed"), NotificationsPromptsListChanged("notifications/prompts/list_changed"), + NotificationsTasksStatus("notifications/tasks/status"), ToolsList("tools/list"), ToolsCall("tools/call"), LoggingSetLevel("logging/setLevel"), @@ -39,6 +40,10 @@ public sealed interface Method { CompletionComplete("completion/complete"), RootsList("roots/list"), ElicitationCreate("elicitation/create"), + TasksGet("tasks/get"), + TasksResult("tasks/result"), + TasksList("tasks/list"), + TasksCancel("tasks/cancel"), } /** diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt index 8eb4e4bae..cb1aa9908 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt @@ -348,3 +348,48 @@ public data class ToolListChangedNotification(override val params: BaseNotificat @EncodeDefault override val method: Method = Method.Defined.NotificationsToolsListChanged } + +// ============================================================================ +// Task Status Notification +// ============================================================================ + +/** + * An optional notification from the receiver to the requestor, informing them that a task’s status has changed. + * Receivers are not required to send these notifications. + * + * This notification can be sent by either side (both [ClientNotification] and [ServerNotification]). + * + * @property params The task status notification parameters containing the current task state. + */ +@Serializable +public data class TaskStatusNotification(override val params: TaskStatusNotificationParams? = null) : + ClientNotification, + ServerNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsTasksStatus +} + +/** + * Parameters for a notifications/tasks/status notification. + * + * @property taskId The task identifier. + * @property status Current task state. + * @property statusMessage Optional human-readable message describing the current task state. + * @property createdAt ISO 8601 timestamp when the task was created. + * @property lastUpdatedAt ISO 8601 timestamp when the task was last updated. + * @property ttl Actual retention duration from creation in milliseconds, null for unlimited. + * @property pollInterval Suggested polling interval in milliseconds. + * @property meta Optional metadata for this notification. + */ +@Serializable +public data class TaskStatusNotificationParams( + override val taskId: String, + override val status: TaskStatus, + override val statusMessage: String? = null, + override val createdAt: String, + override val lastUpdatedAt: String, + override val ttl: Long?, + override val pollInterval: Long? = null, + @SerialName("_meta") override val meta: JsonObject? = null, +) : NotificationParams, + TaskFields diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt index 699dfce88..dc7bd3bef 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.long import kotlinx.serialization.json.longOrNull import kotlin.jvm.JvmInline @@ -22,6 +23,17 @@ public value class RequestMeta(public val json: JsonObject) { } } + /** + * The related task metadata, if this request is associated with a task. + * + * @see RelatedTaskMetadata + * @see RELATED_TASK_META_KEY + */ + public val relatedTask: RelatedTaskMetadata? + get() = json[RELATED_TASK_META_KEY]?.let { element -> + McpJson.decodeFromJsonElement(element) + } + /** * Retrieves the value associated with the specified key from the JSON object. * diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt index 4d5974d92..2d20d425d 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt @@ -159,6 +159,10 @@ private val clientRequestDeserializers: Map CreateMessageResult.serializer() "roots" in jsonObject -> ListRootsResult.serializer() "action" in jsonObject -> ElicitResult.serializer() + "task" in jsonObject -> CreateTaskResult.serializer() + "tasks" in jsonObject -> ListTasksResult.serializer() + "taskId" in jsonObject -> GetTaskResult.serializer() else -> null } } @@ -309,6 +322,7 @@ private fun selectClientResultDeserializer(element: JsonElement): Deserializatio * Selects the appropriate deserializer for server results based on JSON content. * Returns null if the structure doesn't match any known server result type. */ +@Suppress("CyclomaticComplexMethod") private fun selectServerResultDeserializer(element: JsonElement): DeserializationStrategy? { val jsonObject = element.jsonObject return when { @@ -321,6 +335,9 @@ private fun selectServerResultDeserializer(element: JsonElement): Deserializatio "messages" in jsonObject -> GetPromptResult.serializer() "contents" in jsonObject -> ReadResourceResult.serializer() "content" in jsonObject -> CallToolResult.serializer() + "task" in jsonObject -> CreateTaskResult.serializer() + "tasks" in jsonObject -> ListTasksResult.serializer() + "taskId" in jsonObject -> GetTaskResult.serializer() else -> null } } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt new file mode 100644 index 000000000..42b5752af --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt @@ -0,0 +1,347 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlin.jvm.JvmInline + +/** + * The key used in `_meta` to associate a message with a task. + */ +public const val RELATED_TASK_META_KEY: String = "io.modelcontextprotocol/related-task" + +/** + * Common fields shared by all types representing the task state. + */ +public sealed interface TaskFields { + public val taskId: String + public val status: TaskStatus + public val statusMessage: String? + public val createdAt: String + public val lastUpdatedAt: String + public val ttl: Long? + public val pollInterval: Long? +} + +/** + * Data associated with a task. + * + * @property taskId The task identifier. + * @property status Current task state. + * @property statusMessage Optional human-readable message describing the current task state. + * This can provide context for any status, including reasons for "cancelled" status, + * summaries for "completed" status, or diagnostic information for "failed" status. + * @property createdAt ISO 8601 timestamp when the task was created. + * @property lastUpdatedAt ISO 8601 timestamp when the task was last updated. + * @property ttl Actual retention duration from creation in milliseconds, null for unlimited. + * @property pollInterval Suggested polling interval in milliseconds. + */ +@Serializable +public data class Task( + override val taskId: String, + override val status: TaskStatus, + override val statusMessage: String? = null, + override val createdAt: String, + override val lastUpdatedAt: String, + override val ttl: Long?, + override val pollInterval: Long? = null, +) : TaskFields + +/** + * Metadata for augmenting a request with task execution. + * Include this in the `task` field of the request parameters. + * + * @property ttl Requested duration in milliseconds to retain task from creation. + */ +@Serializable +public data class TaskMetadata(val ttl: Long? = null) + +/** + * The status of a task. + */ +@Serializable +public enum class TaskStatus { + @SerialName("working") + Working, + + @SerialName("input_required") + InputRequired, + + @SerialName("completed") + Completed, + + @SerialName("failed") + Failed, + + @SerialName("cancelled") + Cancelled, +} + +/** + * Metadata for associating messages with a task. + * Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`. + * + * @property taskId The task identifier this message is associated with. + */ +@Serializable +public data class RelatedTaskMetadata(val taskId: String) + +/** + * A response to a task-augmented request. + * + * @property task The task data associated with the response. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class CreateTaskResult( + val task: Task, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ClientResult, + ServerResult + +// ============================================================================ +// tasks/get +// ============================================================================ + +/** + * A request to retrieve the state of a task. + * + * This request can be sent by either side (both [ClientRequest] and [ServerRequest]). + * + * @property params The parameters specifying which task to retrieve. + */ +@Serializable +public data class GetTaskRequest(override val params: GetTaskRequestParams) : + ClientRequest, + ServerRequest { + @EncodeDefault + override val method: Method = Method.Defined.TasksGet + + /** + * The task identifier to query. + */ + public val taskId: String + get() = params.taskId + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a tasks/get request. + * + * @property taskId The task identifier to query. + * @property meta Optional metadata for this request. + */ +@Serializable +public data class GetTaskRequestParams( + val taskId: String, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +/** + * The response to a tasks/get request. + * + * Contains all [Task] fields flattened into the result (intersection of Result and Task). + * + * @property taskId The task identifier. + * @property status Current task state. + * @property statusMessage Optional human-readable message describing the current task state. + * @property createdAt ISO 8601 timestamp when the task was created. + * @property lastUpdatedAt ISO 8601 timestamp when the task was last updated. + * @property ttl Actual retention duration from creation in milliseconds, null for unlimited. + * @property pollInterval Suggested polling interval in milliseconds. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class GetTaskResult( + override val taskId: String, + override val status: TaskStatus, + override val statusMessage: String? = null, + override val createdAt: String, + override val lastUpdatedAt: String, + override val ttl: Long?, + override val pollInterval: Long? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ClientResult, + ServerResult, + TaskFields + +// ============================================================================ +// tasks/result +// ============================================================================ + +/** + * A request to retrieve the result of a completed task. + * + * This request can be sent by either side (both [ClientRequest] and [ServerRequest]). + * + * @property params The parameters specifying which task to retrieve results for. + */ +@Serializable +public data class GetTaskPayloadRequest(override val params: GetTaskPayloadRequestParams) : + ClientRequest, + ServerRequest { + @EncodeDefault + override val method: Method = Method.Defined.TasksResult + + /** + * The task identifier to retrieve results for. + */ + public val taskId: String + get() = params.taskId + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a tasks/result request. + * + * @property taskId The task identifier to retrieve results for. + * @property meta Optional metadata for this request. + */ +@Serializable +public data class GetTaskPayloadRequestParams( + val taskId: String, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +/** + * The response to a tasks/result request. + * + * The structure matches the result type of the original request. + * For example, a tools/call task would return the [CallToolResult] structure. + * + * @property meta Optional metadata for this response. + */ +@JvmInline +@Serializable +public value class GetTaskPayloadResult(public val json: JsonObject) : + ClientResult, + ServerResult { + override val meta: JsonObject? + get() = json["_meta"]?.takeIf { it is JsonObject } as? JsonObject + + /** + * Retrieves the value associated with the specified key from the result payload. + * + * @param key the key whose corresponding value is to be returned + * @return the [JsonElement] associated with the specified key, or null if the key does not exist + */ + public operator fun get(key: String): JsonElement? = json[key] +} + +// ============================================================================ +// tasks/list +// ============================================================================ + +/** + * A request to retrieve a list of tasks. + * + * This request can be sent by either side (both [ClientRequest] and [ServerRequest]). + * Supports pagination through the cursor parameter. + * + * @property params Optional pagination parameters to control which page of results to return. + */ +@Serializable +public data class ListTasksRequest(override val params: PaginatedRequestParams? = null) : + ClientRequest, + ServerRequest, + PaginatedRequest { + @EncodeDefault + override val method: Method = Method.Defined.TasksList +} + +/** + * The response to a tasks/list request. + * + * @property tasks The list of tasks. + * @property nextCursor An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. + * @property meta Optional metadata for this response. + */ +@Serializable +public data class ListTasksResult( + val tasks: List, + override val nextCursor: String? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : ClientResult, + ServerResult, + PaginatedResult + +// ============================================================================ +// tasks/cancel +// ============================================================================ + +/** + * A request to cancel a task. + * + * This request can be sent by either side (both [ClientRequest] and [ServerRequest]). + * + * @property params The parameters specifying which task to cancel. + */ +@Serializable +public data class CancelTaskRequest(override val params: CancelTaskRequestParams) : + ClientRequest, + ServerRequest { + @EncodeDefault + override val method: Method = Method.Defined.TasksCancel + + /** + * The task identifier to cancel. + */ + public val taskId: String + get() = params.taskId + + /** + * Metadata for this request. May include a progressToken for out-of-band progress notifications. + */ + public val meta: RequestMeta? + get() = params.meta +} + +/** + * Parameters for a tasks/cancel request. + * + * @property taskId The task identifier to cancel. + * @property meta Optional metadata for this request. + */ +@Serializable +public data class CancelTaskRequestParams( + val taskId: String, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : RequestParams + +/** + * The response to a tasks/cancel request. + * + * Contains all [Task] fields flattened into the result (intersection of Result and Task). + * + * @property taskId The task identifier. + * @property status Current task state. + * @property statusMessage Optional human-readable message describing the current task state. + * @property createdAt ISO 8601 timestamp when the task was created. + * @property lastUpdatedAt ISO 8601 timestamp when the task was last updated. + * @property ttl Actual retention duration from creation in milliseconds, null for unlimited. + * @property pollInterval Suggested polling interval in milliseconds. + * @property meta Optional metadata for this response. + */ +public typealias CancelTaskResult = GetTaskResult diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt index b55a44133..01c6b87a0 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt @@ -310,4 +310,116 @@ class NotificationTest { """.trimIndent(), ) } + + @Test + fun `should serialize TaskStatusNotification with all fields`() { + val notification = TaskStatusNotification( + TaskStatusNotificationParams( + taskId = "task-1", + status = TaskStatus.Working, + statusMessage = "Processing data", + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:01:00Z", + ttl = 60000, + pollInterval = 5000, + meta = buildJsonObject { put("source", "worker-1") }, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/tasks/status", + "params": { + "taskId": "task-1", + "status": "working", + "statusMessage": "Processing data", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:01:00Z", + "ttl": 60000, + "pollInterval": 5000, + "_meta": { + "source": "worker-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize TaskStatusNotification with minimal fields`() { + val notification = TaskStatusNotification( + TaskStatusNotificationParams( + taskId = "task-2", + status = TaskStatus.Completed, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:02:00Z", + ttl = null, + ), + ) + + verifySerialization( + notification, + McpJson, + """ + { + "method": "notifications/tasks/status", + "params": { + "taskId": "task-2", + "status": "completed", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:02:00Z" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize TaskStatusNotification`() { + val json = """ + { + "method": "notifications/tasks/status", + "params": { + "taskId": "task-3", + "status": "failed", + "statusMessage": "Connection lost", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:03:00Z", + "ttl": 30000, + "pollInterval": 1000 + } + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + val params = notification.params!! + + assertEquals(Method.Defined.NotificationsTasksStatus, notification.method) + assertEquals("task-3", params.taskId) + assertEquals(TaskStatus.Failed, params.status) + assertEquals("Connection lost", params.statusMessage) + assertEquals("2025-01-01T00:00:00Z", params.createdAt) + assertEquals("2025-01-01T00:03:00Z", params.lastUpdatedAt) + assertEquals(30000L, params.ttl) + assertEquals(1000L, params.pollInterval) + assertNull(params.meta) + } + + @Test + fun `should deserialize TaskStatusNotification without params`() { + val json = """ + { + "method": "notifications/tasks/status" + } + """.trimIndent() + + val notification = verifyDeserialization(McpJson, json) + + assertEquals(Method.Defined.NotificationsTasksStatus, notification.method) + assertNull(notification.params) + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/TasksTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/TasksTest.kt new file mode 100644 index 000000000..14bd24893 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/TasksTest.kt @@ -0,0 +1,802 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test + +class TasksTest { + + // ======================================================================== + // Task + // ======================================================================== + + @Test + fun `should serialize Task with minimal fields`() { + val task = Task( + taskId = "task-1", + status = TaskStatus.Working, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:00:00Z", + ttl = null, + ) + + verifySerialization( + task, + McpJson, + """ + { + "taskId": "task-1", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:00Z" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize Task with all fields`() { + val task = Task( + taskId = "task-2", + status = TaskStatus.Completed, + statusMessage = "Processing complete", + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:01:00Z", + ttl = 60000, + pollInterval = 5000, + ) + + verifySerialization( + task, + McpJson, + """ + { + "taskId": "task-2", + "status": "completed", + "statusMessage": "Processing complete", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:01:00Z", + "ttl": 60000, + "pollInterval": 5000 + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize Task and verify TaskFields contract`() { + val json = """ + { + "taskId": "task-3", + "status": "failed", + "statusMessage": "Connection lost", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:02:00Z", + "ttl": 30000, + "pollInterval": 1000 + } + """.trimIndent() + + val task = verifyDeserialization(McpJson, json) + task.shouldBeInstanceOf() + task.taskId shouldBe "task-3" + task.status shouldBe TaskStatus.Failed + task.statusMessage shouldBe "Connection lost" + task.createdAt shouldBe "2025-01-01T00:00:00Z" + task.lastUpdatedAt shouldBe "2025-01-01T00:02:00Z" + task.ttl shouldBe 30000L + task.pollInterval shouldBe 1000L + } + + // ======================================================================== + // TaskStatus + // ======================================================================== + + @Test + fun `should serialize all TaskStatus values`() { + verifySerialization(TaskStatus.Working, McpJson, "\"working\"") + verifySerialization(TaskStatus.InputRequired, McpJson, "\"input_required\"") + verifySerialization(TaskStatus.Completed, McpJson, "\"completed\"") + verifySerialization(TaskStatus.Failed, McpJson, "\"failed\"") + verifySerialization(TaskStatus.Cancelled, McpJson, "\"cancelled\"") + } + + // ======================================================================== + // TaskMetadata + // ======================================================================== + + @Test + fun `should serialize TaskMetadata with ttl`() { + val metadata = TaskMetadata(ttl = 120000) + + verifySerialization( + metadata, + McpJson, + """ + { + "ttl": 120000 + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize TaskMetadata without ttl`() { + val metadata = TaskMetadata() + + verifySerialization( + metadata, + McpJson, + "{}", + ) + } + + // ======================================================================== + // RelatedTaskMetadata + // ======================================================================== + + @Test + fun `should serialize RelatedTaskMetadata`() { + val related = RelatedTaskMetadata(taskId = "task-42") + + verifySerialization( + related, + McpJson, + """ + { + "taskId": "task-42" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize RequestMeta with RelatedTaskMetadata`() { + val json = """ + { + "io.modelcontextprotocol/related-task": { + "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840" + } + } + """.trimIndent() + + val meta = McpJson.decodeFromString(json) + val related = meta.relatedTask + related.shouldNotBeNull() + related.taskId shouldBe "786512e2-9e0d-44bd-8f29-789f320fe840" + } + + // ======================================================================== + // CreateTaskResult + // ======================================================================== + + @Test + fun `should serialize CreateTaskResult`() { + val result = CreateTaskResult( + task = Task( + taskId = "task-1", + status = TaskStatus.Working, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:00:00Z", + ttl = null, + ), + ) + + verifySerialization( + result, + McpJson, + """ + { + "task": { + "taskId": "task-1", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:00Z" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize CreateTaskResult with meta`() { + val result = CreateTaskResult( + task = Task( + taskId = "task-1", + status = TaskStatus.Working, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:00:00Z", + ttl = 60000, + ), + meta = buildJsonObject { put("trace", "abc") }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "task": { + "taskId": "task-1", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:00Z", + "ttl": 60000 + }, + "_meta": { + "trace": "abc" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CreateTaskResult`() { + val json = """ + { + "task": { + "taskId": "task-5", + "status": "input_required", + "statusMessage": "Need more data", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:01:00Z", + "pollInterval": 2000 + } + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + result.task.taskId shouldBe "task-5" + result.task.status shouldBe TaskStatus.InputRequired + result.task.statusMessage shouldBe "Need more data" + result.task.pollInterval shouldBe 2000L + result.task.ttl.shouldBeNull() + result.meta.shouldBeNull() + } + + // ======================================================================== + // GetTaskRequest + // ======================================================================== + + @Test + fun `should serialize GetTaskRequest`() { + val request = GetTaskRequest( + GetTaskRequestParams(taskId = "task-10"), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/get", + "params": { + "taskId": "task-10" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize GetTaskRequest with meta`() { + val request = GetTaskRequest( + GetTaskRequestParams( + taskId = "task-10", + meta = RequestMeta( + buildJsonObject { put("progressToken", "pt-1") }, + ), + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/get", + "params": { + "taskId": "task-10", + "_meta": { + "progressToken": "pt-1" + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize GetTaskRequest`() { + val json = """ + { + "method": "tasks/get", + "params": { + "taskId": "task-11" + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + request.method shouldBe Method.Defined.TasksGet + request.taskId shouldBe "task-11" + request.meta.shouldBeNull() + } + + // ======================================================================== + // GetTaskResult + // ======================================================================== + + @Test + fun `should serialize GetTaskResult with all fields`() { + val result = GetTaskResult( + taskId = "task-20", + status = TaskStatus.Completed, + statusMessage = "Done", + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:05:00Z", + ttl = 300000, + pollInterval = 10000, + meta = buildJsonObject { put("server", "main") }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "taskId": "task-20", + "status": "completed", + "statusMessage": "Done", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:05:00Z", + "ttl": 300000, + "pollInterval": 10000, + "_meta": { + "server": "main" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize GetTaskResult and verify TaskFields contract`() { + val json = """ + { + "taskId": "task-21", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:30Z" + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + result.shouldBeInstanceOf() + result.taskId shouldBe "task-21" + result.status shouldBe TaskStatus.Working + result.statusMessage.shouldBeNull() + result.ttl.shouldBeNull() + result.pollInterval.shouldBeNull() + result.meta.shouldBeNull() + } + + // ======================================================================== + // GetTaskPayloadRequest + // ======================================================================== + + @Test + fun `should serialize GetTaskPayloadRequest`() { + val request = GetTaskPayloadRequest( + GetTaskPayloadRequestParams(taskId = "task-30"), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/result", + "params": { + "taskId": "task-30" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize GetTaskPayloadRequest`() { + val json = """ + { + "method": "tasks/result", + "params": { + "taskId": "task-31" + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + request.method shouldBe Method.Defined.TasksResult + request.taskId shouldBe "task-31" + request.meta.shouldBeNull() + } + + // ======================================================================== + // GetTaskPayloadResult + // ======================================================================== + + @Test + fun `should serialize GetTaskPayloadResult with meta`() { + val result = GetTaskPayloadResult( + buildJsonObject { + put("_meta", buildJsonObject { put("origin", "task-30") }) + }, + ) + + verifySerialization( + result, + McpJson, + """ + { + "_meta": { + "origin": "task-30" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize GetTaskPayloadResult without meta`() { + val result = GetTaskPayloadResult(buildJsonObject { }) + + verifySerialization(result, McpJson, "{}") + } + + @Test + fun `should preserve arbitrary payload fields in GetTaskPayloadResult`() { + val json = """ + { + "_meta": { + "origin": "task-42" + }, + "content": [ + { + "type": "text", + "text": "hello" + } + ], + "isError": false + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + result.meta.shouldNotBeNull() + result.meta?.get("origin")?.jsonPrimitive?.content shouldBe "task-42" + result["content"].shouldNotBeNull() + result["isError"].shouldNotBeNull() + } + + // ======================================================================== + // ListTasksRequest + // ======================================================================== + + @Test + fun `should serialize ListTasksRequest without params`() { + val request = ListTasksRequest() + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/list" + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListTasksRequest with cursor`() { + val request = ListTasksRequest( + params = PaginatedRequestParams(cursor = "page-2"), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/list", + "params": { + "cursor": "page-2" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ListTasksRequest`() { + val json = """ + { + "method": "tasks/list", + "params": { + "cursor": "next-page" + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + request.method shouldBe Method.Defined.TasksList + request.cursor shouldBe "next-page" + } + + // ======================================================================== + // ListTasksResult + // ======================================================================== + + @Test + fun `should serialize ListTasksResult with empty list`() { + val result = ListTasksResult(tasks = emptyList()) + + verifySerialization( + result, + McpJson, + """ + { + "tasks": [] + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ListTasksResult with tasks and pagination`() { + val result = ListTasksResult( + tasks = listOf( + Task( + taskId = "task-1", + status = TaskStatus.Working, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:00:00Z", + ttl = null, + ), + Task( + taskId = "task-2", + status = TaskStatus.Completed, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:01:00Z", + ttl = 60000, + ), + ), + nextCursor = "cursor-abc", + ) + + verifySerialization( + result, + McpJson, + """ + { + "tasks": [ + { + "taskId": "task-1", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:00Z" + }, + { + "taskId": "task-2", + "status": "completed", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:01:00Z", + "ttl": 60000 + } + ], + "nextCursor": "cursor-abc" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ListTasksResult`() { + val json = """ + { + "tasks": [ + { + "taskId": "task-99", + "status": "cancelled", + "statusMessage": "User cancelled", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:03:00Z" + } + ] + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + result.tasks.size shouldBe 1 + result.tasks[0].taskId shouldBe "task-99" + result.tasks[0].status shouldBe TaskStatus.Cancelled + result.tasks[0].statusMessage shouldBe "User cancelled" + result.tasks[0].ttl.shouldBeNull() + result.nextCursor.shouldBeNull() + } + + // ======================================================================== + // CancelTaskRequest + // ======================================================================== + + @Test + fun `should serialize CancelTaskRequest`() { + val request = CancelTaskRequest( + CancelTaskRequestParams(taskId = "task-40"), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "tasks/cancel", + "params": { + "taskId": "task-40" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CancelTaskRequest`() { + val json = """ + { + "method": "tasks/cancel", + "params": { + "taskId": "task-41" + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + request.method shouldBe Method.Defined.TasksCancel + request.taskId shouldBe "task-41" + request.meta.shouldBeNull() + } + + // ======================================================================== + // CancelTaskResult + // ======================================================================== + + @Test + fun `should serialize CancelTaskResult`() { + val result = CancelTaskResult( + taskId = "task-40", + status = TaskStatus.Cancelled, + statusMessage = "Cancelled by user", + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:02:00Z", + ttl = null, + ) + + verifySerialization( + result, + McpJson, + """ + { + "taskId": "task-40", + "status": "cancelled", + "statusMessage": "Cancelled by user", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:02:00Z" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize CancelTaskResult and verify TaskFields contract`() { + val json = """ + { + "taskId": "task-42", + "status": "cancelled", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:04:00Z", + "ttl": 120000, + "pollInterval": 3000 + } + """.trimIndent() + + val result = verifyDeserialization(McpJson, json) + result.shouldBeInstanceOf() + result.taskId shouldBe "task-42" + result.status shouldBe TaskStatus.Cancelled + result.statusMessage.shouldBeNull() + result.ttl shouldBe 120000L + result.pollInterval shouldBe 3000L + } + + // ======================================================================== + // Deserialization with unknown fields (ignoreUnknownKeys) + // ======================================================================== + + @Test + fun `should deserialize Task ignoring unknown fields`() { + val json = """ + { + "taskId": "task-100", + "status": "working", + "createdAt": "2025-01-01T00:00:00Z", + "lastUpdatedAt": "2025-01-01T00:00:00Z", + "unknownField": "should be ignored" + } + """.trimIndent() + + val task = McpJson.decodeFromString(json) + task.taskId shouldBe "task-100" + task.status shouldBe TaskStatus.Working + task.ttl.shouldBeNull() + } + + // ======================================================================== + // RequestMeta.relatedTask accessor + // ======================================================================== + + @Test + fun `should read relatedTask from RequestMeta`() { + val meta = RequestMeta( + buildJsonObject { + put( + RELATED_TASK_META_KEY, + buildJsonObject { put("taskId", "task-50") }, + ) + }, + ) + + val related = meta.relatedTask + related.shouldNotBeNull() + related.taskId shouldBe "task-50" + } + + @Test + fun `should return null relatedTask when key is absent`() { + val meta = RequestMeta( + buildJsonObject { put("progressToken", "pt-1") }, + ) + + meta.relatedTask.shouldBeNull() + } + + @Test + fun `GetTaskResult cast to CancelTaskResult via generic does not throw due to type erasure`() { + val result: RequestResult = GetTaskResult( + taskId = "task-1", + status = TaskStatus.Cancelled, + createdAt = "2025-01-01T00:00:00Z", + lastUpdatedAt = "2025-01-01T00:01:00Z", + ttl = null, + ) + + // Simulates what Protocol.request() does: + // the cast goes through a generic type parameter, so JVM erases T to RequestResult + fun uncheckedCast(value: RequestResult): T { + @Suppress("UNCHECKED_CAST") + return value as T + } + + val cancelResult: CancelTaskResult = uncheckedCast(result) + + cancelResult.taskId shouldBe "task-1" + cancelResult.status shouldBe TaskStatus.Cancelled + } +} From 55afb63c27f31920c75c51c091582861bdff21e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:18:41 +0200 Subject: [PATCH 031/187] chore(deps): bump gradle-wrapper from 9.3.1 to 9.4.1 (#614) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [gradle-wrapper](https://github.com/gradle/gradle) from 9.3.1 to 9.4.1.

Release notes

Sourced from gradle-wrapper's releases.

9.4.1

The Gradle team is excited to announce Gradle 9.4.1.

Here are the highlights of this release:

  • Java 26 support
  • Non-class-based JVM tests
  • Enhanced console progress bar

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: akankshaa-00, Attila Kelemen, Björn Kautler, dblood, Dennis Rieks, duvvuvenkataramana, John Burns, Julian, kevinstembridge, Niels Doucet, Philip Wedemann, ploober, Richard Hernandez, Roberto Perez Alcolea, Sebastian Lövdahl, stephan2405, Stephane Landelle, Ujwal Suresh Vanjare, Victor Merkulov, Vincent Potuček, Vladimir Sitnikov.

Upgrade instructions

Switch your build to use Gradle 9.4.1 by updating your wrapper:

./gradlew wrapper --gradle-version=9.4.1 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=gradle-wrapper&package-manager=gradle&previous-version=9.3.1&new-version=9.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.jar | Bin 46175 -> 48966 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 61285a659d17295f1de7c53e24fdf13ad755c379..d997cfc60f4cff0e7451d19d49a82fa986695d07 100644 GIT binary patch delta 39855 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

SdO`o?U^bCPz6Ehh!k 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


Xdyd*p!l1Y@nCA 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f78a6af..c61a118f7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index adff685a0..739907dfd 100755 --- a/gradlew +++ b/gradlew @@ -57,7 +57,7 @@ # 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 +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/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/. From 85d92e3fef1c9ecba329499247a15fb919161534 Mon Sep 17 00:00:00 2001 From: Prachi Pandey Date: Wed, 25 Mar 2026 10:40:14 +0530 Subject: [PATCH 032/187] fix: redact clientSecret, idToken and assertion from options toString() --- .../DiscoverAndRequestJwtAuthGrantOptions.kt | 8 ++- .../auth/ExchangeJwtBearerGrantOptions.kt | 7 +- .../client/auth/RequestJwtAuthGrantOptions.kt | 7 +- .../sdk/client/auth/EnterpriseAuthTest.kt | 65 +++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt index 1d513268b..fac4e7ebd 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt @@ -29,4 +29,10 @@ public data class DiscoverAndRequestJwtAuthGrantOptions( public val audience: String? = null, public val resource: String? = null, public val scope: String? = null, -) +) { + override fun toString(): String = + "DiscoverAndRequestJwtAuthGrantOptions(idpUrl=$idpUrl, idToken=, " + + "clientId=$clientId, idpTokenEndpoint=$idpTokenEndpoint, " + + "clientSecret=${if (clientSecret != null) "" else "null"}, " + + "audience=$audience, resource=$resource, scope=$scope)" +} diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt index 5b2234c1e..f7b72f6ce 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt @@ -26,4 +26,9 @@ public data class ExchangeJwtBearerGrantOptions( public val clientId: String, public val clientSecret: String? = null, public val scope: String? = null, -) +) { + override fun toString(): String = + "ExchangeJwtBearerGrantOptions(tokenEndpoint=$tokenEndpoint, assertion=, " + + "clientId=$clientId, clientSecret=${if (clientSecret != null) "" else "null"}, " + + "scope=$scope)" +} diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt index 634e87656..9d5324560 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt @@ -25,4 +25,9 @@ public data class RequestJwtAuthGrantOptions( public val audience: String? = null, public val resource: String? = null, public val scope: String? = null, -) +) { + override fun toString(): String = + "RequestJwtAuthGrantOptions(tokenEndpoint=$tokenEndpoint, idToken=, " + + "clientId=$clientId, clientSecret=${if (clientSecret != null) "" else "null"}, " + + "audience=$audience, resource=$resource, scope=$scope)" +} diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt index f2c40db18..a1443afea 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt @@ -378,6 +378,71 @@ class EnterpriseAuthTest { ex.message shouldContain "HTTP 500" } + // ----------------------------------------------------------------------- + // toString() — sensitive fields must be redacted + // ----------------------------------------------------------------------- + + @Test + fun `RequestJwtAuthGrantOptions toString redacts clientSecret and idToken`() { + val opts = RequestJwtAuthGrantOptions( + tokenEndpoint = "https://idp.example.com/token", + idToken = "super-secret-id-token", + clientId = "my-client", + clientSecret = "super-secret", + ) + val str = opts.toString() + str shouldContain "tokenEndpoint=https://idp.example.com/token" + str shouldContain "clientId=my-client" + str shouldContain "idToken=" + str shouldContain "clientSecret=" + str shouldNotContain "super-secret-id-token" + str shouldNotContain "super-secret" + } + + @Test + fun `RequestJwtAuthGrantOptions toString shows null for absent clientSecret`() { + val opts = RequestJwtAuthGrantOptions( + tokenEndpoint = "https://idp.example.com/token", + idToken = "tok", + clientId = "client", + ) + opts.toString() shouldContain "clientSecret=null" + } + + @Test + fun `DiscoverAndRequestJwtAuthGrantOptions toString redacts clientSecret and idToken`() { + val opts = DiscoverAndRequestJwtAuthGrantOptions( + idpUrl = "https://idp.example.com", + idToken = "super-secret-id-token", + clientId = "my-client", + clientSecret = "super-secret", + ) + val str = opts.toString() + str shouldContain "idpUrl=https://idp.example.com" + str shouldContain "clientId=my-client" + str shouldContain "idToken=" + str shouldContain "clientSecret=" + str shouldNotContain "super-secret-id-token" + str shouldNotContain "super-secret" + } + + @Test + fun `ExchangeJwtBearerGrantOptions toString redacts assertion and clientSecret`() { + val opts = ExchangeJwtBearerGrantOptions( + tokenEndpoint = "https://auth.example.com/token", + assertion = "secret-jag-token", + clientId = "my-client", + clientSecret = "super-secret", + ) + val str = opts.toString() + str shouldContain "tokenEndpoint=https://auth.example.com/token" + str shouldContain "clientId=my-client" + str shouldContain "assertion=" + str shouldContain "clientSecret=" + str shouldNotContain "secret-jag-token" + str shouldNotContain "super-secret" + } + // ----------------------------------------------------------------------- // Serialization coverage — all fields of the response models // ----------------------------------------------------------------------- From 558399039c37c10fc451601d27e1d0a07977e5a4 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Thu, 26 Mar 2026 11:41:58 +0100 Subject: [PATCH 033/187] chore: deprecation cycle (#615) Remove all deprecation with error level ## Motivation and Context next minor release ## How Has This Been Tested? - ## Breaking Changes Removing it is a binary-incompatible change ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- kotlin-sdk-core/api/kotlin-sdk-core.api | 495 ---- .../kotlin/sdk/shared/Protocol.kt | 19 - .../modelcontextprotocol/kotlin/sdk/types.kt | 2250 ----------------- .../kotlin/sdk/types.util.kt | 42 - .../kotlin/sdk/types/common.kt | 9 - .../kotlin/sdk/types/completion.kt | 12 - .../kotlin/sdk/types/prompts.kt | 37 - .../kotlin/sdk/types/resources.kt | 60 - .../kotlin/sdk/types/roots.kt | 9 - .../kotlin/sdk/types/tools.kt | 20 - kotlin-sdk-server/api/kotlin-sdk-server.api | 2 - .../kotlin/sdk/server/Server.kt | 32 - 12 files changed, 2987 deletions(-) delete mode 100644 kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt delete mode 100644 kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index 7e1b98c50..e7cd0b869 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -1,144 +1,6 @@ -public final class io/modelcontextprotocol/kotlin/sdk/CallToolResult { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CallToolResult; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/CallToolResultBase { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CallToolResultBase$Companion; - public abstract fun getContent ()Ljava/util/List; - public abstract fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject; - public fun isError ()Ljava/lang/Boolean; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CallToolResultBase$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CallToolResultBase$DefaultImpls { - public static fun isError (Lio/modelcontextprotocol/kotlin/sdk/CallToolResultBase;)Ljava/lang/Boolean; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CancelledNotification { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification; - public final fun Params (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams; - public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/CancelledNotification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ClientCapabilities { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities; - public final fun Roots (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; - public static synthetic fun Roots$default (Lio/modelcontextprotocol/kotlin/sdk/ClientCapabilities;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult$Companion; - public fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getContent ()Ljava/util/List; - public final fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject; - public final fun getToolResult ()Lkotlinx/serialization/json/JsonObject; - public final fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public final fun isError ()Ljava/lang/Boolean; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/CompatibilityCallToolResult; - 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/CompatibilityCallToolResult;)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/CompatibilityCallToolResult$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompleteRequest { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest; - public final fun Argument (Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CompleteResult { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CompleteResult; - public final fun Completion (Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion; - public static synthetic fun Completion$default (Lio/modelcontextprotocol/kotlin/sdk/CompleteResult;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest; - public final fun RequestedSchema (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; - public static synthetic fun RequestedSchema$default (Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationRequest;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationResult { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateElicitationResult$Action; - public final fun getAccept ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; - public final fun getCancel ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; - public final fun getDecline ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; -} - -public final class io/modelcontextprotocol/kotlin/sdk/CreateMessageRequest { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/CreateMessageRequest; - public final fun getIncludeContext ()Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext$Companion; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/ErrorCode { - public abstract fun getCode ()I -} - -public final class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Defined; - public final fun getConnectionClosed ()I - public final fun getInternalError ()I - public final fun getInvalidParams ()I - public final fun getInvalidRequest ()I - public final fun getMethodNotFound ()I - public final fun getParseError ()I - public final fun getRequestTimeout ()I -} - -public final class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown : io/modelcontextprotocol/kotlin/sdk/ErrorCode { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown$Companion; - public fun (I)V - public final fun component1 ()I - public final fun copy (I)Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown;IILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown; - public fun equals (Ljava/lang/Object;)Z - public fun getCode ()I - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/ErrorCode$Unknown; - 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/ErrorCode$Unknown;)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/ErrorCode$Unknown$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi : java/lang/annotation/Annotation { } -public final class io/modelcontextprotocol/kotlin/sdk/InitializedNotification { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification; - public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; - public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/InitializedNotification;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; -} - public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/InternalMcpApi : java/lang/annotation/Annotation { } @@ -146,267 +8,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/LibVersionKt { public static final field LIB_VERSION Ljava/lang/String; } -public final class io/modelcontextprotocol/kotlin/sdk/LoggingLevel { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/LoggingLevel; - public final fun getAlert ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; - public final fun getCritical ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; - public final fun getDebug ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; - public final fun getEmergency ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; - public final fun getError ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; - public final fun getInfo ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; - public final fun getNotice ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; - public final fun getWarning ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; -} - -public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification; - public final fun Params (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams; - public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams; - public final fun SetLevelRequest (Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest; -} - -public abstract interface class io/modelcontextprotocol/kotlin/sdk/Method { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Method$Companion; - public abstract fun getValue ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Method$Companion { - public final fun Custom (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/Method$Custom; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Method$Defined { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Method$Defined; - public final fun getCompletionComplete ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getElicitationCreate ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getInitialize ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getLoggingSetLevel ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getNotificationsCancelled ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getNotificationsInitialized ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getNotificationsMessage ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getNotificationsProgress ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getNotificationsPromptsListChanged ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getNotificationsResourcesListChanged ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getNotificationsResourcesUpdated ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getNotificationsRootsListChanged ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getNotificationsToolsListChanged ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getPing ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getPromptsGet ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getPromptsList ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getResourcesList ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getResourcesRead ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getResourcesSubscribe ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getResourcesTemplatesList ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getResourcesUnsubscribe ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getRootsList ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getSamplingCreateMessage ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getToolsCall ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; - public final fun getToolsList ()Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; -} - -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 ()D - public abstract fun getTotal ()Ljava/lang/Double; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ProgressBase$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification; - public final fun Params (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;DLjava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams; - public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/ProgressNotification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;DLjava/lang/Double;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams; -} - -public final class io/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification; - public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; - public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/PromptListChangedNotification;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; -} - -public final class io/modelcontextprotocol/kotlin/sdk/RequestId { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/RequestId; - public final fun NumberId-Nt6Vbyc (J)J - public final fun StringId-QNUQp7g (Ljava/lang/String;)Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification; - public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; - public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceListChangedNotification;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification; - public final fun Params (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams; - public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/ResourceUpdatedNotification;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Role { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Role; - public final fun getAssistant ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; - public final fun getUser ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; -} - -public final class io/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification; - public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; - public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/RootsListChangedNotification;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ServerCapabilities { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities; - public final fun Prompts (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; - public static synthetic fun Prompts$default (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; - public final fun Resources (Ljava/lang/Boolean;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; - public static synthetic fun Resources$default (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Ljava/lang/Boolean;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; - public final fun Tools (Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; - public static synthetic fun Tools$default (Lio/modelcontextprotocol/kotlin/sdk/ServerCapabilities;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Tool { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/Tool; - public final fun Input (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; - public static synthetic fun Input$default (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; - public final fun Output (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; - public static synthetic fun Output$default (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; -} - -public final class io/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification; - public final fun Params (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; - public static synthetic fun Params$default (Lio/modelcontextprotocol/kotlin/sdk/ToolListChangedNotification;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; -} - -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 CallToolResult (Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; - public static synthetic fun CallToolResult$default (Ljava/util/List;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; - public static final fun CancelledNotification (Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CancelledNotification; - public static final fun ClientCapabilities (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; - public static synthetic fun ClientCapabilities$default (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; - public static final fun CompleteRequest (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest; - public static final fun CompleteResult (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult; - public static synthetic fun CompleteResult$default (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult; - public static final fun CreateElicitationRequest (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest; - public static final fun CreateElicitationResult (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult; - public static synthetic fun CreateElicitationResult$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult; - public static final fun CreateMessageRequest (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest; - public static final fun InitializedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializedNotification; - public static synthetic fun InitializedNotification$default (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/InitializedNotification; - public static final fun LoggingMessageNotification (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification; - public static final fun ProgressNotification (Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ProgressNotification; - public static final fun PromptListChangedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/PromptListChangedNotification; - public static final fun ResourceListChangedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification; - public static synthetic fun ResourceListChangedNotification$default (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceListChangedNotification; - public static final fun ResourceUpdatedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification; - public static final fun RootsListChangedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification; - public static synthetic fun RootsListChangedNotification$default (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/RootsListChangedNotification; - public static final fun ServerCapabilities (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; - public static synthetic fun ServerCapabilities$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; - public static final fun Tool (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/Tool; - public static synthetic fun Tool$default (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/Tool; - public static final fun ToolListChangedNotification (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotification; - public static final fun getSUPPORTED_PROTOCOL_VERSIONS ()Ljava/util/List; - public static final fun toJSON (Lio/modelcontextprotocol/kotlin/sdk/types/Notification;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCNotification; - public static final fun toJSON (Lio/modelcontextprotocol/kotlin/sdk/types/Request;)Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCRequest; -} - -public final class io/modelcontextprotocol/kotlin/sdk/Types_utilKt { - public static final fun error (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; - public static synthetic fun error$default (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; - public static final fun getEmptyJsonObject ()Lkotlinx/serialization/json/JsonObject; - public static final fun ok (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; - public static synthetic fun ok$default (Lio/modelcontextprotocol/kotlin/sdk/CallToolResult;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; -} - -public final class io/modelcontextprotocol/kotlin/sdk/UnknownContent { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownContent$Companion; - public fun (Ljava/lang/String;)V - public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/UnknownContent; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownContent;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownContent; - public fun equals (Ljava/lang/Object;)Z - public final fun getType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/UnknownContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/UnknownContent$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/UnknownContent; - 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/UnknownContent;)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/UnknownContent$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Method;Lio/modelcontextprotocol/kotlin/sdk/types/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/types/NotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; - public fun equals (Ljava/lang/Object;)Z - public final fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/Method; - public final fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/UnknownMethodRequestOrNotification; - 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/UnknownMethodRequestOrNotification;)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/UnknownMethodRequestOrNotification$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/UnknownReference { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/UnknownReference$Companion; - public fun (Ljava/lang/String;)V - public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/UnknownReference; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/UnknownReference;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/UnknownReference; - public fun equals (Ljava/lang/Object;)Z - public final fun getType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/UnknownReference$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/UnknownReference$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/UnknownReference; - 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/UnknownReference;)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/UnknownReference$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - public final class io/modelcontextprotocol/kotlin/sdk/internal/Utils_jvmKt { public static final fun getIODispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; } @@ -483,7 +84,6 @@ 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; } public class io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { @@ -617,7 +217,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/AudioContent : io/mo public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getMimeType ()Ljava/lang/String; public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -656,7 +255,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/BaseNotificationPara public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BaseNotificationParams; public fun equals (Ljava/lang/Object;)Z public fun getMeta ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -719,7 +317,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getMimeType ()Ljava/lang/String; public fun getUri ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -827,7 +424,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolResult : io/ public final fun getContent ()Ljava/util/List; public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public final fun isError ()Ljava/lang/Boolean; public fun toString ()Ljava/lang/String; @@ -954,7 +550,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotificatio public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getReason ()Ljava/lang/String; public final fun getRequestId ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1077,10 +672,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ClientResult$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/types/ClientResult$DefaultImpls { - public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/ClientResult;)Lkotlinx/serialization/json/JsonObject; -} - public final class io/modelcontextprotocol/kotlin/sdk/types/CommonKt { public static final field DEFAULT_NEGOTIATED_PROTOCOL_VERSION Ljava/lang/String; public static final field LATEST_PROTOCOL_VERSION Ljava/lang/String; @@ -1091,8 +682,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CommonKt { public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest$Companion; - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument;Lio/modelcontextprotocol/kotlin/sdk/types/Reference;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Argument;Lio/modelcontextprotocol/kotlin/sdk/types/Reference;Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams$Context;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams; public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest; @@ -1237,7 +826,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteResult : io/ public fun equals (Ljava/lang/Object;)Z public final fun getCompletion ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteResult$Completion; public fun getMeta ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1302,10 +890,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ContentBlock$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/types/ContentBlock$DefaultImpls { - public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/ContentBlock;)Lkotlinx/serialization/json/JsonObject; -} - public final class io/modelcontextprotocol/kotlin/sdk/types/ContentTypes : java/lang/Enum { public static final field AUDIO Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes$Companion; @@ -1444,7 +1028,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult public final fun getModel ()Ljava/lang/String; public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; public final fun getStopReason-6olV-UY ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1475,7 +1058,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateTaskResult : i public fun equals (Ljava/lang/Object;)Z public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/Task; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1679,7 +1261,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitResult : io/mo public final fun getAction ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Action; public final fun getContent ()Lkotlinx/serialization/json/JsonObject; public fun getMeta ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1731,7 +1312,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/EmbeddedResource : i public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getResource ()Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents; public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1761,7 +1341,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/EmptyResult : io/mod public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/EmptyResult;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/EmptyResult; public fun equals (Ljava/lang/Object;)Z public fun getMeta ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1784,8 +1363,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/EmptyResult$Companio public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams; public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest; public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest;Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest; @@ -1870,7 +1447,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptResult : io public final fun getDescription ()Ljava/lang/String; public final fun getMessages ()Ljava/util/List; public fun getMeta ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1962,8 +1538,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskPayloadResult public final fun getJson ()Lkotlinx/serialization/json/JsonObject; public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public static fun getMeta-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; - public static fun get_meta-impl (Lkotlinx/serialization/json/JsonObject;)Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I public fun toString ()Ljava/lang/String; @@ -2070,7 +1644,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/GetTaskResult : io/m public fun getStatusMessage ()Ljava/lang/String; public fun getTaskId ()Ljava/lang/String; public fun getTtl ()Ljava/lang/Long; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2153,7 +1726,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ImageContent : io/mo public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getMimeType ()Ljava/lang/String; public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2329,7 +1901,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeResult : i public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getProtocolVersion ()Ljava/lang/String; public final fun getServerInfo ()Lio/modelcontextprotocol/kotlin/sdk/types/Implementation; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2553,8 +2124,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest : public fun ()V public fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)V public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest; public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest;Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest; @@ -2602,7 +2171,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult : public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getNextCursor ()Ljava/lang/String; public final fun getPrompts ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2627,8 +2195,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplate public fun ()V public fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)V public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest; public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest;Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest; @@ -2676,7 +2242,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplate public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getNextCursor ()Ljava/lang/String; public final fun getResourceTemplates ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2701,8 +2266,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest public fun ()V public fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)V public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest; public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest;Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest; @@ -2750,7 +2313,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getNextCursor ()Ljava/lang/String; public final fun getResources ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2775,7 +2337,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest : i public fun ()V public fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;)V public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams; public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest; public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;Lio/modelcontextprotocol/kotlin/sdk/types/BaseRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest; @@ -2820,7 +2381,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsResult : io public fun equals (Ljava/lang/Object;)Z public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getRoots ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2886,7 +2446,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListTasksResult : io public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getNextCursor ()Ljava/lang/String; public final fun getTasks ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2911,8 +2470,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest : i public fun ()V public fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)V public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams; public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest; public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest;Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest; @@ -2960,7 +2517,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsResult : io public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getNextCursor ()Ljava/lang/String; public final fun getTools ()Ljava/util/List; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3043,7 +2599,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotifi public final fun getLevel ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; public final fun getLogger ()Ljava/lang/String; public fun getMeta ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3088,10 +2643,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/MediaContent$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/types/MediaContent$DefaultImpls { - public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)Lkotlinx/serialization/json/JsonObject; -} - public abstract class io/modelcontextprotocol/kotlin/sdk/types/MediaContentBuilder { public fun ()V public final fun annotations (Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;)V @@ -3263,10 +2814,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/NotificationParams$C public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/types/NotificationParams$DefaultImpls { - public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams;)Lkotlinx/serialization/json/JsonObject; -} - public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest : io/modelcontextprotocol/kotlin/sdk/types/Request { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest$Companion; public fun getCursor ()Ljava/lang/String; @@ -3328,10 +2875,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult$Comp public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult$DefaultImpls { - public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedResult;)Lkotlinx/serialization/json/JsonObject; -} - public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest$Companion; public fun ()V @@ -3444,7 +2987,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ProgressNotification public final fun getProgress ()D public final fun getProgressToken ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; public final fun getTotal ()Ljava/lang/Double; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3483,7 +3025,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Prompt : io/modelcon public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getName ()Ljava/lang/String; public final fun getTitle ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3680,8 +3221,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RPCError$ErrorCode { public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams; public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest; public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest;Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest; @@ -3759,7 +3298,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceResult : public fun equals (Ljava/lang/Object;)Z public final fun getContents ()Ljava/util/List; public fun getMeta ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -3977,10 +3515,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RequestResult$Compan public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/types/RequestResult$DefaultImpls { - public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/RequestResult;)Lkotlinx/serialization/json/JsonObject; -} - public final class io/modelcontextprotocol/kotlin/sdk/types/Resource : io/modelcontextprotocol/kotlin/sdk/types/ResourceLike { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Resource$Companion; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V @@ -4006,7 +3540,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Resource : io/modelc public final fun getSize ()Ljava/lang/Long; public final fun getTitle ()Ljava/lang/String; 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; } @@ -4036,10 +3569,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceContents$Com public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceContents$DefaultImpls { - public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceContents;)Lkotlinx/serialization/json/JsonObject; -} - public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ResourceLike : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLike$Companion; } @@ -4048,10 +3577,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceLike$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceLike$DefaultImpls { - public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLike;)Lkotlinx/serialization/json/JsonObject; -} - public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceLink : io/modelcontextprotocol/kotlin/sdk/types/ContentBlock, io/modelcontextprotocol/kotlin/sdk/types/ResourceLike { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ResourceLink$Companion; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Annotations;Lkotlinx/serialization/json/JsonObject;)V @@ -4078,7 +3603,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceLink : io/mo public final fun getTitle ()Ljava/lang/String; public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; 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; } @@ -4152,7 +3676,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceTemplate : i public final fun getName ()Ljava/lang/String; public final fun getTitle ()Ljava/lang/String; public final fun getUriTemplate ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -4240,7 +3763,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotif public fun equals (Ljava/lang/Object;)Z public fun getMeta ()Lkotlinx/serialization/json/JsonObject; 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; } @@ -4294,7 +3816,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Root : io/modelconte public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getName ()Ljava/lang/String; 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; } @@ -4551,10 +4072,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ServerResult$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/types/ServerResult$DefaultImpls { - public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/ServerResult;)Lkotlinx/serialization/json/JsonObject; -} - public final class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParams;)V @@ -4660,8 +4177,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/StopReason$Companion public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams; public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest; public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest; @@ -4877,7 +4392,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/TaskStatusNotificati public fun getStatusMessage ()Ljava/lang/String; public fun getTaskId ()Ljava/lang/String; public fun getTtl ()Ljava/lang/Long; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -4929,7 +4443,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/TextContent : io/mod public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getText ()Ljava/lang/String; public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -4971,7 +4484,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/TextResourceContents public fun getMimeType ()Ljava/lang/String; public final fun getText ()Ljava/lang/String; public fun getUri ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -5016,7 +4528,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Tool : io/modelconte public final fun getName ()Ljava/lang/String; public final fun getOutputSchema ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; public final fun getTitle ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -5192,7 +4703,6 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/UnknownResourceConte public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public fun getMimeType ()Ljava/lang/String; public fun getUri ()Ljava/lang/String; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -5286,17 +4796,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestPa public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/WithMeta { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/WithMeta$Companion; public abstract fun getMeta ()Lkotlinx/serialization/json/JsonObject; - public fun get_meta ()Lkotlinx/serialization/json/JsonObject; } public final class io/modelcontextprotocol/kotlin/sdk/types/WithMeta$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class io/modelcontextprotocol/kotlin/sdk/types/WithMeta$DefaultImpls { - public static fun get_meta (Lio/modelcontextprotocol/kotlin/sdk/types/WithMeta;)Lkotlinx/serialization/json/JsonObject; -} - public final class io/modelcontextprotocol/kotlin/sdk/utils/MatchResult { public fun (Ljava/util/Map;I)V public fun equals (Ljava/lang/Object;)Z diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 0e3db35cd..20739e0ad 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -33,9 +33,6 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withTimeout -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.ClassDiscriminatorMode -import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.encodeToJsonElement @@ -52,22 +49,6 @@ public const val IMPLEMENTATION_NAME: String = "mcp-ktor" */ public typealias ProgressCallback = (Progress) -> Unit -@OptIn(ExperimentalSerializationApi::class) -@Deprecated( - message = "Use McpJson instead", - replaceWith = ReplaceWith("McpJson", "io.modelcontextprotocol.kotlin.sdk.types.McpJson"), - level = DeprecationLevel.ERROR, -) -public val McpJson: Json by lazy { - Json { - ignoreUnknownKeys = true - encodeDefaults = true - isLenient = true - classDiscriminatorMode = ClassDiscriminatorMode.NONE - explicitNulls = false - } -} - /** * Additional initialization options. */ diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt deleted file mode 100644 index 3b2b2d2b6..000000000 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.kt +++ /dev/null @@ -1,2250 +0,0 @@ -@file:Suppress("FunctionName", "TooManyFunctions") - -package io.modelcontextprotocol.kotlin.sdk - -import io.modelcontextprotocol.kotlin.sdk.types.ContentBlock -import io.modelcontextprotocol.kotlin.sdk.types.fromJSON -import io.modelcontextprotocol.kotlin.sdk.types.toJSON -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities.Roots as TypesRoots - -@Deprecated( - message = "Use `LATEST_PROTOCOL_VERSION` instead", - replaceWith = ReplaceWith( - "LATEST_PROTOCOL_VERSION", - "io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION", - ), - level = DeprecationLevel.ERROR, -) -public const val LATEST_PROTOCOL_VERSION: String = io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION - -@Deprecated( - message = "Use `SUPPORTED_PROTOCOL_VERSIONS` instead", - replaceWith = ReplaceWith( - "SUPPORTED_PROTOCOL_VERSIONS", - "io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS", - ), - level = DeprecationLevel.ERROR, -) -public val SUPPORTED_PROTOCOL_VERSIONS: List = - io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS - -@Deprecated( - message = "Use `JSONRPC_VERSION` instead", - replaceWith = ReplaceWith("JSONRPC_VERSION", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPC_VERSION"), - level = DeprecationLevel.ERROR, -) -public const val JSONRPC_VERSION: String = io.modelcontextprotocol.kotlin.sdk.types.JSONRPC_VERSION - -/** - * A progress token, used to associate progress notifications with the original request. - * Stores message ID. - */ -@Deprecated( - message = "Use `ProgressToken` instead", - replaceWith = ReplaceWith("ProgressToken", "io.modelcontextprotocol.kotlin.sdk.types.ProgressToken"), - level = DeprecationLevel.ERROR, -) -public typealias ProgressToken = io.modelcontextprotocol.kotlin.sdk.types.ProgressToken - -/** - * An opaque token used to represent a cursor for pagination. - */ -@Deprecated( - message = "This alias will be removed. Use String directly instead.", - replaceWith = ReplaceWith("String"), - level = DeprecationLevel.ERROR, -) -public typealias Cursor = String - -/** - * Represents an entity that includes additional metadata in its responses. - */ -@Deprecated( - message = "Use `WithMeta` instead", - replaceWith = ReplaceWith("WithMeta", "io.modelcontextprotocol.kotlin.sdk.types.WithMeta"), - level = DeprecationLevel.ERROR, -) -public typealias WithMeta = io.modelcontextprotocol.kotlin.sdk.types.WithMeta - -/** - * An implementation of [WithMeta] containing custom metadata. - */ -@Deprecated( - message = "This class will be removed. Use `WithMeta` instead", - replaceWith = ReplaceWith("WithMeta", "io.modelcontextprotocol.kotlin.sdk.types.WithMeta"), - level = DeprecationLevel.ERROR, -) -public typealias CustomMeta = io.modelcontextprotocol.kotlin.sdk.types.WithMeta - -/** - * Represents a method in the protocol, which can be predefined or custom. - */ -@Deprecated( - message = "Use `Method` instead", - replaceWith = ReplaceWith("Method", "io.modelcontextprotocol.kotlin.sdk.types.Method"), - level = DeprecationLevel.ERROR, -) -public sealed interface Method { - public val value: String - - public companion object { - /** - * Represents a custom method defined by the user. - */ - @Deprecated( - message = "Use `Method.Custom` instead", - replaceWith = ReplaceWith("Method.Custom", "io.modelcontextprotocol.kotlin.sdk.types.Method.Custom"), - level = DeprecationLevel.ERROR, - ) - public fun Custom(value: String): io.modelcontextprotocol.kotlin.sdk.types.Method.Custom = - io.modelcontextprotocol.kotlin.sdk.types.Method.Custom(value) - } - - public object Defined { - @Deprecated( - message = "Use `Method.Defined.Initialize` instead", - replaceWith = ReplaceWith( - "Method.Defined.Initialize", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.Initialize", - ), - level = DeprecationLevel.ERROR, - ) - public val Initialize: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.Initialize - - @Deprecated( - message = "Use `Method.Defined.Ping` instead", - replaceWith = ReplaceWith( - "Method.Defined.Ping", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.Ping", - ), - level = DeprecationLevel.ERROR, - ) - public val Ping: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.Ping - - @Deprecated( - message = "Use `Method.Defined.ResourcesList` instead", - replaceWith = ReplaceWith( - "Method.Defined.ResourcesList", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesList", - ), - level = DeprecationLevel.ERROR, - ) - public val ResourcesList: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesList - - @Deprecated( - message = "Use `Method.Defined.ResourcesTemplatesList` instead", - replaceWith = ReplaceWith( - "Method.Defined.ResourcesTemplatesList", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesTemplatesList", - ), - level = DeprecationLevel.ERROR, - ) - public val ResourcesTemplatesList: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesTemplatesList - - @Deprecated( - message = "Use `Method.Defined.ResourcesRead` instead", - replaceWith = ReplaceWith( - "Method.Defined.ResourcesRead", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesRead", - ), - level = DeprecationLevel.ERROR, - ) - public val ResourcesRead: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesRead - - @Deprecated( - message = "Use `Method.Defined.ResourcesSubscribe` instead", - replaceWith = ReplaceWith( - "Method.Defined.ResourcesSubscribe", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesSubscribe", - ), - level = DeprecationLevel.ERROR, - ) - public val ResourcesSubscribe: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesSubscribe - - @Deprecated( - message = "Use `Method.Defined.ResourcesUnsubscribe` instead", - replaceWith = ReplaceWith( - "Method.Defined.ResourcesUnsubscribe", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesUnsubscribe", - ), - level = DeprecationLevel.ERROR, - ) - public val ResourcesUnsubscribe: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ResourcesUnsubscribe - - @Deprecated( - message = "Use `Method.Defined.PromptsList` instead", - replaceWith = ReplaceWith( - "Method.Defined.PromptsList", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.PromptsList", - ), - level = DeprecationLevel.ERROR, - ) - public val PromptsList: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.PromptsList - - @Deprecated( - message = "Use `Method.Defined.PromptsGet` instead", - replaceWith = ReplaceWith( - "Method.Defined.PromptsGet", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.PromptsGet", - ), - level = DeprecationLevel.ERROR, - ) - public val PromptsGet: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.PromptsGet - - @Deprecated( - message = "Use `Method.Defined.NotificationsCancelled` instead", - replaceWith = ReplaceWith( - "Method.Defined.NotificationsCancelled", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsCancelled", - ), - level = DeprecationLevel.ERROR, - ) - public val NotificationsCancelled: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsCancelled - - @Deprecated( - message = "Use `Method.Defined.NotificationsInitialized` instead", - replaceWith = ReplaceWith( - "Method.Defined.NotificationsInitialized", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsInitialized", - ), - level = DeprecationLevel.ERROR, - ) - public val NotificationsInitialized: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsInitialized - - @Deprecated( - message = "Use `Method.Defined.NotificationsProgress` instead", - replaceWith = ReplaceWith( - "Method.Defined.NotificationsProgress", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsProgress", - ), - level = DeprecationLevel.ERROR, - ) - public val NotificationsProgress: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsProgress - - @Deprecated( - message = "Use `Method.Defined.NotificationsMessage` instead", - replaceWith = ReplaceWith( - "Method.Defined.NotificationsMessage", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsMessage", - ), - level = DeprecationLevel.ERROR, - ) - public val NotificationsMessage: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsMessage - - @Deprecated( - message = "Use `Method.Defined.NotificationsResourcesUpdated` instead", - replaceWith = ReplaceWith( - "Method.Defined.NotificationsResourcesUpdated", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsResourcesUpdated", - ), - level = DeprecationLevel.ERROR, - ) - public val NotificationsResourcesUpdated: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsResourcesUpdated - - @Deprecated( - message = "Use `Method.Defined.NotificationsResourcesListChanged` instead", - replaceWith = ReplaceWith( - "Method.Defined.NotificationsResourcesListChanged", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsResourcesListChanged", - ), - level = DeprecationLevel.ERROR, - ) - public val NotificationsResourcesListChanged: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsResourcesListChanged - - @Deprecated( - message = "Use `Method.Defined.NotificationsToolsListChanged` instead", - replaceWith = ReplaceWith( - "Method.Defined.NotificationsToolsListChanged", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsToolsListChanged", - ), - level = DeprecationLevel.ERROR, - ) - public val NotificationsToolsListChanged: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsToolsListChanged - - @Deprecated( - message = "Use `Method.Defined.NotificationsRootsListChanged` instead", - replaceWith = ReplaceWith( - "Method.Defined.NotificationsRootsListChanged", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsRootsListChanged", - ), - level = DeprecationLevel.ERROR, - ) - public val NotificationsRootsListChanged: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsRootsListChanged - - @Deprecated( - message = "Use `Method.Defined.NotificationsPromptsListChanged` instead", - replaceWith = ReplaceWith( - "Method.Defined.NotificationsPromptsListChanged", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsPromptsListChanged", - ), - level = DeprecationLevel.ERROR, - ) - public val NotificationsPromptsListChanged: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.NotificationsPromptsListChanged - - @Deprecated( - message = "Use `Method.Defined.ToolsList` instead", - replaceWith = ReplaceWith( - "Method.Defined.ToolsList", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ToolsList", - ), - level = DeprecationLevel.ERROR, - ) - public val ToolsList: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ToolsList - - @Deprecated( - message = "Use `Method.Defined.ToolsCall` instead", - replaceWith = ReplaceWith( - "Method.Defined.ToolsCall", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ToolsCall", - ), - level = DeprecationLevel.ERROR, - ) - public val ToolsCall: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ToolsCall - - @Deprecated( - message = "Use `Method.Defined.LoggingSetLevel` instead", - replaceWith = ReplaceWith( - "Method.Defined.LoggingSetLevel", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.LoggingSetLevel", - ), - level = DeprecationLevel.ERROR, - ) - public val LoggingSetLevel: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.LoggingSetLevel - - @Deprecated( - message = "Use `Method.Defined.SamplingCreateMessage` instead", - replaceWith = ReplaceWith( - "Method.Defined.SamplingCreateMessage", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.SamplingCreateMessage", - ), - level = DeprecationLevel.ERROR, - ) - public val SamplingCreateMessage: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.SamplingCreateMessage - - @Deprecated( - message = "Use `Method.Defined.CompletionComplete` instead", - replaceWith = ReplaceWith( - "Method.Defined.CompletionComplete", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.CompletionComplete", - ), - level = DeprecationLevel.ERROR, - ) - public val CompletionComplete: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.CompletionComplete - - @Deprecated( - message = "Use `Method.Defined.RootsList` instead", - replaceWith = ReplaceWith( - "Method.Defined.RootsList", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.RootsList", - ), - level = DeprecationLevel.ERROR, - ) - public val RootsList: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.RootsList - - @Deprecated( - message = "Use `Method.Defined.ElicitationCreate` instead", - replaceWith = ReplaceWith( - "Method.Defined.ElicitationCreate", - "io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ElicitationCreate", - ), - level = DeprecationLevel.ERROR, - ) - public val ElicitationCreate: io.modelcontextprotocol.kotlin.sdk.types.Method.Defined = - io.modelcontextprotocol.kotlin.sdk.types.Method.Defined.ElicitationCreate - } -} - -/** - * Represents a request in the protocol. - */ -@Deprecated( - message = "Use `Request` instead", - replaceWith = ReplaceWith("Request", "io.modelcontextprotocol.kotlin.sdk.types.Request"), - level = DeprecationLevel.ERROR, -) -public typealias Request = io.modelcontextprotocol.kotlin.sdk.types.Request - -/** - * Converts the request to a JSON-RPC request. - * - * @return The JSON-RPC request representation. - */ -@Deprecated( - message = "Use `toJSON` instead", - replaceWith = ReplaceWith("toJSON", "io.modelcontextprotocol.kotlin.sdk.types.toJSON"), - level = DeprecationLevel.ERROR, -) -@Suppress("DEPRECATION_ERROR") -public fun Request.toJSON(): JSONRPCRequest = this.toJSON() - -/** - * Decodes a JSON-RPC request into a protocol-specific [Request]. - * - * @return The decoded [Request] or null - */ -@Deprecated( - message = "Use `fromJSON` instead", - replaceWith = ReplaceWith("fromJSON", "io.modelcontextprotocol.kotlin.sdk.types.fromJSON"), - level = DeprecationLevel.ERROR, -) -@Suppress("DEPRECATION_ERROR") -internal fun JSONRPCRequest.fromJSON(): Request = this.fromJSON() - -/** - * A custom request with a specified method. - */ -@Deprecated( - message = "Use `CustomRequest` instead", - replaceWith = ReplaceWith("CustomRequest", "io.modelcontextprotocol.kotlin.sdk.types.CustomRequest"), - level = DeprecationLevel.ERROR, -) -public typealias CustomRequest = io.modelcontextprotocol.kotlin.sdk.types.CustomRequest - -/** - * Represents a notification in the protocol. - */ -@Deprecated( - message = "Use `Notification` instead", - replaceWith = ReplaceWith("Notification", "io.modelcontextprotocol.kotlin.sdk.types.Notification"), - level = DeprecationLevel.ERROR, -) -public typealias Notification = io.modelcontextprotocol.kotlin.sdk.types.Notification - -/** - * Converts the notification to a JSON-RPC notification. - * - * @return The JSON-RPC notification representation. - */ -@Deprecated( - message = "Use `toJSON` instead", - replaceWith = ReplaceWith("toJSON", "io.modelcontextprotocol.kotlin.sdk.types.toJSON"), - level = DeprecationLevel.ERROR, -) -@Suppress("DEPRECATION_ERROR") -public fun Notification.toJSON(): JSONRPCNotification = this.toJSON() - -/** - * Decodes a JSON-RPC notification into a protocol-specific [Notification]. - * - * @return The decoded [Notification]. - */ -@Deprecated( - message = "Use `fromJSON` instead", - replaceWith = ReplaceWith("toJSON", "io.modelcontextprotocol.kotlin.sdk.types.fromJSON"), - level = DeprecationLevel.ERROR, -) -@Suppress("DEPRECATION_ERROR") -internal fun JSONRPCNotification.fromJSON(): Notification = this.fromJSON() - -/** - * Represents the result of a request, including additional metadata. - */ -@Deprecated( - message = "Use `RequestResult` instead", - replaceWith = ReplaceWith("RequestResult", "io.modelcontextprotocol.kotlin.sdk.types.RequestResult"), - level = DeprecationLevel.ERROR, -) -public typealias RequestResult = io.modelcontextprotocol.kotlin.sdk.types.RequestResult - -/** - * An empty result for a request containing optional metadata. - */ -@Deprecated( - message = "Use `EmptyResult` instead", - replaceWith = ReplaceWith("EmptyResult", "io.modelcontextprotocol.kotlin.sdk.types.EmptyResult"), - level = DeprecationLevel.ERROR, -) -public typealias EmptyRequestResult = io.modelcontextprotocol.kotlin.sdk.types.EmptyResult - -/** - * A uniquely identifying ID for a request in JSON-RPC. - */ -@Deprecated( - message = "Use `RequestId` instead", - replaceWith = ReplaceWith("RequestId", "io.modelcontextprotocol.kotlin.sdk.types.RequestId"), - level = DeprecationLevel.ERROR, -) -public object RequestId { - @Deprecated( - message = "Use `RequestId.StringId` instead", - replaceWith = ReplaceWith("RequestId.StringId", "io.modelcontextprotocol.kotlin.sdk.types.RequestId.StringId"), - level = DeprecationLevel.ERROR, - ) - public fun StringId(value: String): io.modelcontextprotocol.kotlin.sdk.types.RequestId.StringId = - io.modelcontextprotocol.kotlin.sdk.types.RequestId.StringId(value) - - @Deprecated( - message = "Use `RequestId.NumberId` instead", - replaceWith = ReplaceWith("RequestId.NumberId", "io.modelcontextprotocol.kotlin.sdk.types.RequestId.NumberId"), - level = DeprecationLevel.ERROR, - ) - public fun NumberId(value: Long): io.modelcontextprotocol.kotlin.sdk.types.RequestId.NumberId = - io.modelcontextprotocol.kotlin.sdk.types.RequestId.NumberId(value) -} - -/** - * Represents a JSON-RPC message in the protocol. - */ -@Deprecated( - message = "Use `JSONRPCMessage` instead", - replaceWith = ReplaceWith("JSONRPCMessage", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage"), - level = DeprecationLevel.ERROR, -) -public typealias JSONRPCMessage = io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage - -/** - * A request that expects a response. - */ -@Deprecated( - message = "Use `JSONRPCRequest` instead", - replaceWith = ReplaceWith("JSONRPCRequest", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest"), - level = DeprecationLevel.ERROR, -) -public typealias JSONRPCRequest = io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest - -/** - * A notification which does not expect a response. - */ -@Deprecated( - message = "Use `JSONRPCNotification` instead", - replaceWith = ReplaceWith("JSONRPCNotification", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification"), - level = DeprecationLevel.ERROR, -) -public typealias JSONRPCNotification = io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification - -/** - * A successful (non-error) response to a request. - */ -@Deprecated( - message = "Use `JSONRPCResponse` instead", - replaceWith = ReplaceWith("JSONRPCResponse", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse"), - level = DeprecationLevel.ERROR, -) -public typealias JSONRPCResponse = io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse - -/** - * An incomplete set of error codes that may appear in JSON-RPC responses. - */ -@Deprecated( - message = "Use `RPCError` instead", - replaceWith = ReplaceWith("RPCError", "io.modelcontextprotocol.kotlin.sdk.types.RPCError"), - level = DeprecationLevel.ERROR, -) -public sealed interface ErrorCode { - public val code: Int - - public object Defined { - @Deprecated( - message = "Use `RPCError.ErrorCode.CONNECTION_CLOSED` instead", - replaceWith = ReplaceWith( - "RPCError.ErrorCode.CONNECTION_CLOSED", - "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.CONNECTION_CLOSED", - ), - level = DeprecationLevel.ERROR, - ) - public val ConnectionClosed: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.CONNECTION_CLOSED - - @Deprecated( - message = "Use `RPCError.ErrorCode.REQUEST_TIMEOUT` instead", - replaceWith = ReplaceWith( - "RPCError.ErrorCode.REQUEST_TIMEOUT", - "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.REQUEST_TIMEOUT", - ), - level = DeprecationLevel.ERROR, - ) - public val RequestTimeout: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.REQUEST_TIMEOUT - - @Deprecated( - message = "Use `RPCError.ErrorCode.PARSE_ERROR` instead", - replaceWith = ReplaceWith( - "RPCError.ErrorCode.PARSE_ERROR", - "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.PARSE_ERROR", - ), - level = DeprecationLevel.ERROR, - ) - public val ParseError: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.PARSE_ERROR - - @Deprecated( - message = "Use `RPCError.ErrorCode.INVALID_REQUEST` instead", - replaceWith = ReplaceWith( - "RPCError.ErrorCode.INVALID_REQUEST", - "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INVALID_REQUEST", - ), - level = DeprecationLevel.ERROR, - ) - public val InvalidRequest: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INVALID_REQUEST - - @Deprecated( - message = "Use `RPCError.ErrorCode.METHOD_NOT_FOUND` instead", - replaceWith = ReplaceWith( - "RPCError.ErrorCode.METHOD_NOT_FOUND", - "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.METHOD_NOT_FOUND", - ), - level = DeprecationLevel.ERROR, - ) - public val MethodNotFound: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.METHOD_NOT_FOUND - - @Deprecated( - message = "Use `RPCError.ErrorCode.INVALID_PARAMS` instead", - replaceWith = ReplaceWith( - "RPCError.ErrorCode.INVALID_PARAMS", - "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INVALID_PARAMS", - ), - level = DeprecationLevel.ERROR, - ) - public val InvalidParams: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INVALID_PARAMS - - @Deprecated( - message = "Use `RPCError.ErrorCode.INTERNAL_ERROR` instead", - replaceWith = ReplaceWith( - "RPCError.ErrorCode.INTERNAL_ERROR", - "io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INTERNAL_ERROR", - ), - level = DeprecationLevel.ERROR, - ) - public val InternalError: Int = io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.INTERNAL_ERROR - } - - @Deprecated( - message = "Use `RPCError` instead", - replaceWith = ReplaceWith("RPCError", "io.modelcontextprotocol.kotlin.sdk.types.RPCError"), - level = DeprecationLevel.ERROR, - ) - @Serializable - @Suppress("DEPRECATION_ERROR") - public data class Unknown(override val code: Int) : ErrorCode -} - -/** - * A response to a request that indicates an error occurred. - */ -@Deprecated( - message = "Use `JSONRPCError` instead", - replaceWith = ReplaceWith("JSONRPCError", "io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError"), - level = DeprecationLevel.ERROR, -) -public typealias JSONRPCError = io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError - -/** - * Base interface for notification parameters with optional metadata. - */ -@Deprecated( - message = "Use `NotificationParams` instead", - replaceWith = ReplaceWith("NotificationParams", "io.modelcontextprotocol.kotlin.sdk.types.NotificationParams"), - level = DeprecationLevel.ERROR, -) -public typealias NotificationParams = io.modelcontextprotocol.kotlin.sdk.types.NotificationParams - -/* Cancellation */ - -@Deprecated( - message = "Use `CancelledNotification` instead", - replaceWith = ReplaceWith( - "CancelledNotification", - "io.modelcontextprotocol.kotlin.sdk.types.CancelledNotification", - ), - level = DeprecationLevel.ERROR, -) -public object CancelledNotification { - @Deprecated( - message = "Use `CancelledNotificationParams` instead", - replaceWith = ReplaceWith( - "CancelledNotificationParams", - "io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams", - ), - level = DeprecationLevel.ERROR, - ) - public fun Params( - requestId: io.modelcontextprotocol.kotlin.sdk.types.RequestId, - reason: String? = null, - meta: JsonObject? = null, - ): io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams = - io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams(requestId, reason, meta) -} - -/** - * This notification can be sent by either side to indicate that it is cancelling a previously issued request. - * - * The request SHOULD still be in-flight, but due to communication latency, it is always possible that - * this notification MAY arrive after the request has already finished. - * - * This notification indicates that the result will be unused, so any associated processing SHOULD cease. - * - * A client MUST NOT attempt to cancel its `initialize` request. - */ -@Deprecated( - message = "Use `CancelledNotification` instead", - replaceWith = ReplaceWith( - "CancelledNotification", - "io.modelcontextprotocol.kotlin.sdk.types.CancelledNotification", - ), - level = DeprecationLevel.ERROR, -) -public fun CancelledNotification( - params: io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams, -): io.modelcontextprotocol.kotlin.sdk.types.CancelledNotification = - io.modelcontextprotocol.kotlin.sdk.types.CancelledNotification(params) - -/* Initialization */ - -/** - * Describes the name and version of an MCP implementation. - */ -@Deprecated( - message = "Use `Implementation` instead", - replaceWith = ReplaceWith("Implementation", "io.modelcontextprotocol.kotlin.sdk.types.Implementation"), - level = DeprecationLevel.ERROR, -) -public typealias Implementation = io.modelcontextprotocol.kotlin.sdk.types.Implementation - -@Deprecated( - message = "Use `ClientCapabilities` instead", - replaceWith = ReplaceWith("ClientCapabilities", "io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities"), - level = DeprecationLevel.ERROR, -) -public object ClientCapabilities { - /** - * Creates an instance of [Roots], representing the client's capability - * to list roots and optionally listen for changes in the roots list. - * - * @param listChanged Optional flag indicating whether the client supports notifications - * for changes to the roots list. If set to true, the client will notify the server when - * roots are added or removed. - * @return A new instance of [Roots] configured with the provided [listChanged] value. - */ - @Deprecated( - message = "Use `ClientCapabilities.Roots` instead", - replaceWith = ReplaceWith( - "ClientCapabilities.Roots", - "io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities.Roots", - ), - level = DeprecationLevel.ERROR, - ) - public fun Roots(listChanged: Boolean? = null): TypesRoots = TypesRoots(listChanged) -} - -/** - * Capabilities a client may support. - * Known capabilities are defined here, in this, but this is not a closed set: - * any client can define its own, additional capabilities. - */ -@Deprecated( - message = "Use `ClientCapabilities` instead", - replaceWith = ReplaceWith("ClientCapabilities", "io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities"), - level = DeprecationLevel.ERROR, -) -public fun ClientCapabilities( - sampling: JsonObject? = null, - roots: TypesRoots? = null, - elicitation: JsonObject? = null, - experimental: JsonObject? = null, -): io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities = - io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities(sampling, roots, elicitation, experimental) - -/** - * Represents a request sent by the client. - */ -@Deprecated( - message = "Use `ClientRequest` instead", - replaceWith = ReplaceWith("ClientRequest", "io.modelcontextprotocol.kotlin.sdk.types.ClientRequest"), - level = DeprecationLevel.ERROR, -) -public typealias ClientRequest = io.modelcontextprotocol.kotlin.sdk.types.ClientRequest - -/** - * Represents a notification sent by the client. - */ -@Deprecated( - message = "Use `ClientNotification` instead", - replaceWith = ReplaceWith("ClientNotification", "io.modelcontextprotocol.kotlin.sdk.types.ClientNotification"), - level = DeprecationLevel.ERROR, -) -public typealias ClientNotification = io.modelcontextprotocol.kotlin.sdk.types.ClientNotification - -/** - * Represents a result returned to the client. - */ -@Deprecated( - message = "Use `ClientResult` instead", - replaceWith = ReplaceWith("ClientResult", "io.modelcontextprotocol.kotlin.sdk.types.ClientResult"), - level = DeprecationLevel.ERROR, -) -public typealias ClientResult = io.modelcontextprotocol.kotlin.sdk.types.ClientResult - -/** - * Represents a request sent by the server. - */ -@Deprecated( - message = "Use `ServerRequest` instead", - replaceWith = ReplaceWith("ServerRequest", "io.modelcontextprotocol.kotlin.sdk.types.ServerRequest"), - level = DeprecationLevel.ERROR, -) -public typealias ServerRequest = io.modelcontextprotocol.kotlin.sdk.types.ServerRequest - -/** - * Represents a notification sent by the server. - */ -@Deprecated( - message = "Use `ServerNotification` instead", - replaceWith = ReplaceWith("ServerNotification", "io.modelcontextprotocol.kotlin.sdk.types.ServerNotification"), - level = DeprecationLevel.ERROR, -) -public typealias ServerNotification = io.modelcontextprotocol.kotlin.sdk.types.ServerNotification - -/** - * Represents a result returned by the server. - */ -@Deprecated( - message = "Use `ServerResult` instead", - replaceWith = ReplaceWith("ServerResult", "io.modelcontextprotocol.kotlin.sdk.types.ServerResult"), - level = DeprecationLevel.ERROR, -) -public typealias ServerResult = io.modelcontextprotocol.kotlin.sdk.types.ServerResult - -/** - * Represents a request or notification for an unknown method. - * - * @param method The method that is unknown. - */ -@Serializable -@Deprecated( - message = "This class will be removed", - level = DeprecationLevel.ERROR, -) -@Suppress("DEPRECATION_ERROR") -public data class UnknownMethodRequestOrNotification(val method: Method, val params: NotificationParams? = null) - -/** - * This request is sent from the client to the server when it first connects, asking it to begin initialization. - */ -@Deprecated( - message = "Use `InitializeRequest` instead", - replaceWith = ReplaceWith("InitializeRequest", "io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest"), - level = DeprecationLevel.ERROR, -) -public typealias InitializeRequest = io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest - -@Deprecated( - message = "Use `ServerCapabilities` instead", - replaceWith = ReplaceWith("ServerCapabilities", "io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities"), - level = DeprecationLevel.ERROR, -) -public object ServerCapabilities { - - @Deprecated( - message = "Use `ServerCapabilities.Prompts` instead", - replaceWith = ReplaceWith( - "ServerCapabilities.Prompts", - "io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Prompts", - ), - level = DeprecationLevel.ERROR, - ) - public fun Prompts( - listChanged: Boolean? = null, - ): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Prompts = - io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Prompts(listChanged) - - @Deprecated( - message = "Use `ServerCapabilities.Resources` instead", - replaceWith = ReplaceWith( - "ServerCapabilities.Resources", - "io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Resources", - ), - level = DeprecationLevel.ERROR, - ) - public fun Resources( - listChanged: Boolean? = null, - subscribe: Boolean? = null, - ): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Resources = - io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Resources(listChanged, subscribe) - - @Deprecated( - message = "Use `ServerCapabilities.Tools` instead", - replaceWith = ReplaceWith( - "ServerCapabilities.Tools", - "io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Tools", - ), - level = DeprecationLevel.ERROR, - ) - public fun Tools(listChanged: Boolean? = null): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Tools = - io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Tools(listChanged) -} - -/** - * Represents the capabilities that a server can support. - * - * @property experimental Experimental, non-standard capabilities that the server supports. - * @property logging Present if the server supports sending log messages to the client. - * @property prompts Capabilities related to prompt templates offered by the server. - * @property resources Capabilities related to resources available on the server. - * @property tools Capabilities related to tools that can be called on the server. - */ -@Deprecated( - message = "Use `ServerCapabilities` instead", - replaceWith = ReplaceWith("ServerCapabilities", "io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities"), - level = DeprecationLevel.ERROR, -) -@Suppress("LongParameterList") -public fun ServerCapabilities( - tools: io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Tools? = null, - resources: io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Resources? = null, - prompts: io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities.Prompts? = null, - logging: JsonObject? = null, - completions: JsonObject? = null, - experimental: JsonObject? = null, -): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities = - io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities( - tools, - resources, - prompts, - logging, - completions, - experimental, - ) - -/** - * After receiving an initialized request from the client, the server sends this response. - */ -@Deprecated( - message = "Use `InitializeResult` instead", - replaceWith = ReplaceWith("InitializeResult", "io.modelcontextprotocol.kotlin.sdk.types.InitializeResult"), - level = DeprecationLevel.ERROR, -) -public typealias InitializeResult = io.modelcontextprotocol.kotlin.sdk.types.InitializeResult - -@Deprecated( - message = "Use `InitializedNotification` instead", - replaceWith = ReplaceWith( - "InitializedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification", - ), - level = DeprecationLevel.ERROR, -) -public object InitializedNotification { - @Deprecated( - message = "Use `BaseNotificationParams` instead", - replaceWith = ReplaceWith( - "BaseNotificationParams", - "io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams", - ), - level = DeprecationLevel.ERROR, - ) - @Suppress("DEPRECATION_ERROR") - public fun Params( - meta: JsonObject = EmptyJsonObject, - ): io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams = - io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams(meta) -} - -/** - * This notification is sent from the client to the server after initialization has finished. - */ -@Deprecated( - message = "Use `InitializedNotification` instead", - replaceWith = ReplaceWith( - "InitializedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification", - ), - level = DeprecationLevel.ERROR, -) -public fun InitializedNotification( - params: io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams? = null, -): io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification = - io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification(params) - -/* Ping */ - -/** - * A ping, issued by either the server or the client, to check that the other party is still alive. - * The receiver must promptly respond, or else it may be disconnected. - */ -@Deprecated( - message = "Use `PingRequest` instead", - replaceWith = ReplaceWith("PingRequest", "io.modelcontextprotocol.kotlin.sdk.types.PingRequest"), - level = DeprecationLevel.ERROR, -) -public typealias PingRequest = io.modelcontextprotocol.kotlin.sdk.types.PingRequest - -/** - * Represents the base interface for progress tracking. - */ -@Serializable -@Deprecated( - message = "This interface will be removed", - level = DeprecationLevel.ERROR, -) -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: Double - - /** - * Total number of items to a process (or total progress required), if known. - */ - public val total: Double? - - /** - * An optional message describing the current progress. - */ - public val message: String? -} - -/* Progress notifications */ - -/** - * Represents a progress notification. - */ -@Deprecated( - message = "Use `Progress` instead", - replaceWith = ReplaceWith("Progress", "io.modelcontextprotocol.kotlin.sdk.types.Progress"), - level = DeprecationLevel.ERROR, -) -public typealias Progress = io.modelcontextprotocol.kotlin.sdk.types.Progress - -@Deprecated( - message = "Use `ProgressNotification` instead", - replaceWith = ReplaceWith("ProgressNotification", "io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification"), - level = DeprecationLevel.ERROR, -) -public object ProgressNotification { - @Deprecated( - message = "Use `ProgressNotificationParams` instead", - replaceWith = ReplaceWith( - "ProgressNotificationParams", - "io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams", - ), - level = DeprecationLevel.ERROR, - ) - public fun Params( - progressToken: io.modelcontextprotocol.kotlin.sdk.types.ProgressToken, - progress: Double, - total: Double? = null, - message: String? = null, - meta: JsonObject? = null, - ): io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams = - io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams( - progressToken, - progress, - total, - message, - meta, - ) -} - -/** - * An out-of-band notification used to inform the receiver of a progress update for a long-running request. - */ -@Deprecated( - message = "Use `ProgressNotification` instead", - replaceWith = ReplaceWith("ProgressNotification", "io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification"), - level = DeprecationLevel.ERROR, -) -public fun ProgressNotification( - params: io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams, -): io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification = - io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification(params) - -/* Pagination */ - -/** - * Represents a request supporting pagination. - */ -@Deprecated( - message = "Use `PaginatedRequest` instead", - replaceWith = ReplaceWith("PaginatedRequest", "io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequest"), - level = DeprecationLevel.ERROR, -) -public typealias PaginatedRequest = io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequest - -/** - * Represents a paginated result of a request. - */ -@Deprecated( - message = "Use `PaginatedResult` instead", - replaceWith = ReplaceWith("PaginatedResult", "io.modelcontextprotocol.kotlin.sdk.types.PaginatedResult"), - level = DeprecationLevel.ERROR, -) -public typealias PaginatedResult = io.modelcontextprotocol.kotlin.sdk.types.PaginatedResult - -/* Resources */ - -/** - * The contents of a specific resource or sub-resource. - */ -@Deprecated( - message = "Use `ResourceContents` instead", - replaceWith = ReplaceWith("ResourceContents", "io.modelcontextprotocol.kotlin.sdk.types.ResourceContents"), - level = DeprecationLevel.ERROR, -) -public typealias ResourceContents = io.modelcontextprotocol.kotlin.sdk.types.ResourceContents - -/** - * Represents the text contents of a resource. - */ -@Deprecated( - message = "Use `TextResourceContents` instead", - replaceWith = ReplaceWith("TextResourceContents", "io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents"), - level = DeprecationLevel.ERROR, -) -public typealias TextResourceContents = io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents - -/** - * Represents the binary contents of a resource encoded as a base64 string. - */ -@Deprecated( - message = "Use `BlobResourceContents` instead", - replaceWith = ReplaceWith("BlobResourceContents", "io.modelcontextprotocol.kotlin.sdk.types.BlobResourceContents"), - level = DeprecationLevel.ERROR, -) -public typealias BlobResourceContents = io.modelcontextprotocol.kotlin.sdk.types.BlobResourceContents - -/** - * Represents resource contents with unknown or unspecified data. - */ -@Deprecated( - message = "Use `UnknownResourceContents` instead", - replaceWith = ReplaceWith( - "UnknownResourceContents", - "io.modelcontextprotocol.kotlin.sdk.types.UnknownResourceContents", - ), - level = DeprecationLevel.ERROR, -) -public typealias UnknownResourceContents = io.modelcontextprotocol.kotlin.sdk.types.UnknownResourceContents - -/** - * A known resource that the server is capable of reading. - */ -@Deprecated( - message = "Use `Resource` instead", - replaceWith = ReplaceWith("Resource", "io.modelcontextprotocol.kotlin.sdk.types.Resource"), - level = DeprecationLevel.ERROR, -) -public typealias Resource = io.modelcontextprotocol.kotlin.sdk.types.Resource - -/** - * A template description for resources available on the server. - */ -@Deprecated( - message = "Use `ResourceTemplate` instead", - replaceWith = ReplaceWith("ResourceTemplate", "io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate"), - level = DeprecationLevel.ERROR, -) -public typealias ResourceTemplate = io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplate - -/** - * Sent from the client to request a list of resources the server has. - */ -@Deprecated( - message = "Use `ListResourcesRequest` instead", - replaceWith = ReplaceWith("ListResourcesRequest", "io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest"), - level = DeprecationLevel.ERROR, -) -public typealias ListResourcesRequest = io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest - -/** - * The server's response to a resources/list request from the client. - */ -@Deprecated( - message = "Use `ListResourcesResult` instead", - replaceWith = ReplaceWith("ListResourcesResult", "io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult"), - level = DeprecationLevel.ERROR, -) -public typealias ListResourcesResult = io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult - -/** - * Sent from the client to request a list of resource templates the server has. - */ -@Deprecated( - message = "Use `ListResourceTemplatesRequest` instead", - replaceWith = ReplaceWith( - "ListResourceTemplatesRequest", - "io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest", - ), - level = DeprecationLevel.ERROR, -) -public typealias ListResourceTemplatesRequest = io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest - -/** - * The server's response to a resources/templates/list request from the client. - */ -@Deprecated( - message = "Use `ListResourceTemplatesResult` instead", - replaceWith = ReplaceWith( - "ListResourceTemplatesResult", - "io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesResult", - ), - level = DeprecationLevel.ERROR, -) -public typealias ListResourceTemplatesResult = io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesResult - -/** - * Sent from the client to the server to read a specific resource URI. - */ -@Deprecated( - message = "Use `ReadResourceRequest` instead", - replaceWith = ReplaceWith("ReadResourceRequest", "io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest"), - level = DeprecationLevel.ERROR, -) -public typealias ReadResourceRequest = io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest - -/** - * The server's response to a resources/read request from the client. - */ -@Deprecated( - message = "Use `ReadResourceResult` instead", - replaceWith = ReplaceWith("ReadResourceResult", "io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult"), - level = DeprecationLevel.ERROR, -) -public typealias ReadResourceResult = io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult - -@Deprecated( - message = "Use `ResourceListChangedNotification` instead", - replaceWith = ReplaceWith( - "ResourceListChangedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification", - ), - level = DeprecationLevel.ERROR, -) -public object ResourceListChangedNotification { - @Deprecated( - message = "Use `BaseNotificationParams` instead", - replaceWith = ReplaceWith( - "BaseNotificationParams", - "io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams", - ), - level = DeprecationLevel.ERROR, - ) - public fun Params(meta: JsonObject? = null): io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams = - io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams(meta) -} - -/** - * An optional notification from the server to the client, - * informing it that the list of resources it can read from has changed. - * Servers may issue this without any previous subscription from the client. - */ -@Deprecated( - message = "Use `ResourceListChangedNotification` instead", - replaceWith = ReplaceWith( - "ResourceListChangedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification", - ), - level = DeprecationLevel.ERROR, -) -public fun ResourceListChangedNotification( - params: io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams? = null, -): io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification = - io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification(params) - -/** - * Sent from the client to request resources/updated notifications from the server - * whenever a particular resource changes. - */ -@Deprecated( - message = "Use `SubscribeRequest` instead", - replaceWith = ReplaceWith("SubscribeRequest", "io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest"), - level = DeprecationLevel.ERROR, -) -public typealias SubscribeRequest = io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest - -/** - * Sent from the client to request cancellation of resources/updated notifications from the server. - * This should follow a previous resources/subscribe request. - */ -@Deprecated( - message = "Use `UnsubscribeRequest` instead", - replaceWith = ReplaceWith("UnsubscribeRequest", "io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest"), - level = DeprecationLevel.ERROR, -) -public typealias UnsubscribeRequest = io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest - -@Deprecated( - message = "Use `ResourceUpdatedNotification` instead", - replaceWith = ReplaceWith( - "ResourceUpdatedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification", - ), - level = DeprecationLevel.ERROR, -) -public object ResourceUpdatedNotification { - @Deprecated( - message = "Use `ResourceUpdatedNotificationParams` instead", - replaceWith = ReplaceWith( - "ResourceUpdatedNotificationParams", - "io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams", - ), - level = DeprecationLevel.ERROR, - ) - public fun Params( - uri: String, - meta: JsonObject? = null, - ): io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams = - io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams(uri, meta) -} - -/** - * A notification from the server to the client, informing it that a resource has changed and may need to be read again. - * This should only be sent if the client previously sent a resources/subscribe request. - */ -@Deprecated( - message = "Use `ResourceUpdatedNotification` instead", - replaceWith = ReplaceWith( - "ResourceUpdatedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification", - ), - level = DeprecationLevel.ERROR, -) -public fun ResourceUpdatedNotification( - params: io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams, -): io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification = - io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification(params) - -/* Prompts */ - -/** - * Describes an argument that a prompt can accept. - */ -@Deprecated( - message = "Use `PromptArgument` instead", - replaceWith = ReplaceWith("PromptArgument", "io.modelcontextprotocol.kotlin.sdk.types.PromptArgument"), - level = DeprecationLevel.ERROR, -) -public typealias PromptArgument = io.modelcontextprotocol.kotlin.sdk.types.PromptArgument - -/** - * A prompt or prompt template that the server offers. - */ -@Deprecated( - message = "Use `Prompt` instead", - replaceWith = ReplaceWith("Prompt", "io.modelcontextprotocol.kotlin.sdk.types.Prompt"), - level = DeprecationLevel.ERROR, -) -public typealias Prompt = io.modelcontextprotocol.kotlin.sdk.types.Prompt - -/** - * Sent from the client to request a list of prompts and prompt templates the server has. - */ -@Deprecated( - message = "Use `ListPromptsRequest` instead", - replaceWith = ReplaceWith("ListPromptsRequest", "io.modelcontextprotocol.kotlin.sdk.types.ListPromptsRequest"), - level = DeprecationLevel.ERROR, -) -public typealias ListPromptsRequest = io.modelcontextprotocol.kotlin.sdk.types.ListPromptsRequest - -/** - * The server's response to a prompts/list request from the client. - */ -@Deprecated( - message = "Use `ListPromptsResult` instead", - replaceWith = ReplaceWith("ListPromptsResult", "io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult"), - level = DeprecationLevel.ERROR, -) -public typealias ListPromptsResult = io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult - -/** - * Used by the client to get a prompt provided by the server. - */ -@Deprecated( - message = "Use `GetPromptRequest` instead", - replaceWith = ReplaceWith("GetPromptRequest", "io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest"), - level = DeprecationLevel.ERROR, -) -public typealias GetPromptRequest = io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest - -/** - * Represents the content of a prompt message. - */ -@Deprecated( - message = "Use `ContentBlock` instead", - replaceWith = ReplaceWith("ContentBlock"), - level = DeprecationLevel.ERROR, -) -public typealias PromptMessageContent = ContentBlock - -/** - * Represents prompt message content that is either text, image or audio. - */ -@Deprecated( - message = "Use `MediaContent` instead", - replaceWith = ReplaceWith("MediaContent"), - level = DeprecationLevel.ERROR, -) -public typealias PromptMessageContentMultimodal = io.modelcontextprotocol.kotlin.sdk.types.MediaContent - -/** - * Text provided to or from an LLM. - */ -@Deprecated( - message = "Use `TextContent` instead", - replaceWith = ReplaceWith("TextContent", "io.modelcontextprotocol.kotlin.sdk.types.TextContent"), - level = DeprecationLevel.ERROR, -) -public typealias TextContent = io.modelcontextprotocol.kotlin.sdk.types.TextContent - -/** - * An image provided to or from an LLM. - */ -@Deprecated( - message = "Use `ImageContent` instead", - replaceWith = ReplaceWith("ImageContent", "io.modelcontextprotocol.kotlin.sdk.types.ImageContent"), - level = DeprecationLevel.ERROR, -) -public typealias ImageContent = io.modelcontextprotocol.kotlin.sdk.types.ImageContent - -/** - * Audio provided to or from an LLM. - */ -@Deprecated( - message = "Use `AudioContent` instead", - replaceWith = ReplaceWith("AudioContent", "io.modelcontextprotocol.kotlin.sdk.types.AudioContent"), - level = DeprecationLevel.ERROR, -) -public typealias AudioContent = io.modelcontextprotocol.kotlin.sdk.types.AudioContent - -/** - * Unknown content provided to or from an LLM. - */ -@Serializable -@Deprecated( - message = "This class will be removed.", - replaceWith = ReplaceWith("MediaContent"), - level = DeprecationLevel.ERROR, -) -public data class UnknownContent(val type: String) - -/** - * The contents of a resource, embedded into a prompt or tool call result. - */ -@Deprecated( - message = "Use `EmbeddedResource` instead", - replaceWith = ReplaceWith("EmbeddedResource", "io.modelcontextprotocol.kotlin.sdk.types.EmbeddedResource"), - level = DeprecationLevel.ERROR, -) -public typealias EmbeddedResource = io.modelcontextprotocol.kotlin.sdk.types.EmbeddedResource - -/** - * Enum representing the role of a participant. - */ -@Deprecated( - message = "Use `Role` instead", - replaceWith = ReplaceWith("Role", "io.modelcontextprotocol.kotlin.sdk.types.Role"), - level = DeprecationLevel.ERROR, -) -public object Role { - @Deprecated( - message = "Use `Role.User` instead", - replaceWith = ReplaceWith("Role.User", "io.modelcontextprotocol.kotlin.sdk.types.Role"), - level = DeprecationLevel.ERROR, - ) - public val user: io.modelcontextprotocol.kotlin.sdk.types.Role = io.modelcontextprotocol.kotlin.sdk.types.Role.User - - @Deprecated( - message = "Use `Role.Assistant` instead", - replaceWith = ReplaceWith("Role.Assistant", "io.modelcontextprotocol.kotlin.sdk.types.Role"), - level = DeprecationLevel.ERROR, - ) - public val assistant: io.modelcontextprotocol.kotlin.sdk.types.Role = - io.modelcontextprotocol.kotlin.sdk.types.Role.Assistant -} - -/** - * Optional annotations for the client. - * The client can use annotations to inform how objects are used or displayed. - */ -@Deprecated( - message = "Use `Annotations` instead", - replaceWith = ReplaceWith("Annotations", "io.modelcontextprotocol.kotlin.sdk.types.Annotations"), - level = DeprecationLevel.ERROR, -) -public typealias Annotations = io.modelcontextprotocol.kotlin.sdk.types.Annotations - -/** - * Describes a message returned as part of a prompt. - */ -@Deprecated( - message = "Use `PromptMessage` instead", - replaceWith = ReplaceWith("PromptMessage", "io.modelcontextprotocol.kotlin.sdk.types.PromptMessage"), - level = DeprecationLevel.ERROR, -) -public typealias PromptMessage = io.modelcontextprotocol.kotlin.sdk.types.PromptMessage - -/** - * The server's response to a prompts/get request from the client. - */ -@Deprecated( - message = "Use `GetPromptResult` instead", - replaceWith = ReplaceWith("GetPromptResult", "io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult"), - level = DeprecationLevel.ERROR, -) -public typealias GetPromptResult = io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult - -@Deprecated( - message = "Use `PromptListChangedNotification` instead", - replaceWith = ReplaceWith( - "PromptListChangedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification", - ), - level = DeprecationLevel.ERROR, -) -public object PromptListChangedNotification { - @Deprecated( - message = "Use `BaseNotificationParams` instead", - replaceWith = ReplaceWith( - "BaseNotificationParams", - "io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams", - ), - level = DeprecationLevel.ERROR, - ) - public fun Params(meta: JsonObject? = null): io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams = - io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams(meta) -} - -/** - * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. - * Servers may issue this without any previous subscription from the client. - */ -@Deprecated( - message = "Use `PromptListChangedNotification` instead", - replaceWith = ReplaceWith( - "PromptListChangedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification", - ), - level = DeprecationLevel.ERROR, -) -public fun PromptListChangedNotification( - params: io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams, -): io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification = - io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification(params) - -/* Tools */ - -/** - * Additional properties describing a Tool to clients. - * - * NOTE: all properties in ToolAnnotations are **hints**. - * They are not guaranteed to provide a faithful description of - * tool behavior (including descriptive properties like `title`). - * - * Clients should never make tool use decisions based on ToolAnnotations - * received from untrusted servers. - */ -@Deprecated( - message = "Use `ToolAnnotations` instead", - replaceWith = ReplaceWith("ToolAnnotations", "io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations"), - level = DeprecationLevel.ERROR, -) -public typealias ToolAnnotations = io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations - -@Deprecated( - message = "Use `Tool` instead", - replaceWith = ReplaceWith("Tool", "io.modelcontextprotocol.kotlin.sdk.types.Tool"), - level = DeprecationLevel.ERROR, -) -public object Tool { - @Deprecated( - message = "Use `ToolSchema` instead", - replaceWith = ReplaceWith("ToolSchema", "io.modelcontextprotocol.kotlin.sdk.types.ToolSchema"), - level = DeprecationLevel.ERROR, - ) - public fun Input( - properties: JsonObject? = null, - required: List? = null, - ): io.modelcontextprotocol.kotlin.sdk.types.ToolSchema = - io.modelcontextprotocol.kotlin.sdk.types.ToolSchema(properties, required) - - @Deprecated( - message = "Use `ToolSchema` instead", - replaceWith = ReplaceWith("ToolSchema", "io.modelcontextprotocol.kotlin.sdk.types.ToolSchema"), - level = DeprecationLevel.ERROR, - ) - public fun Output( - properties: JsonObject? = null, - required: List? = null, - ): io.modelcontextprotocol.kotlin.sdk.types.ToolSchema = - io.modelcontextprotocol.kotlin.sdk.types.ToolSchema(properties, required) -} - -/** - * Definition for a tool the client can call. - */ -@Deprecated( - message = "Use `Tool` instead", - replaceWith = ReplaceWith("Tool", "io.modelcontextprotocol.kotlin.sdk.types.Tool"), - level = DeprecationLevel.ERROR, -) -@Suppress("LongParameterList", "FunctionParameterNaming") -public fun Tool( - name: String, - inputSchema: io.modelcontextprotocol.kotlin.sdk.types.ToolSchema, - description: String? = null, - outputSchema: io.modelcontextprotocol.kotlin.sdk.types.ToolSchema? = null, - title: String? = null, - annotations: io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations? = null, - icons: List? = null, - _meta: JsonObject? = null, -): io.modelcontextprotocol.kotlin.sdk.types.Tool = io.modelcontextprotocol.kotlin.sdk.types.Tool( - name, - inputSchema, - description, - outputSchema, - title, - annotations, - icons, - meta = _meta, -) - -/** - * Sent from the client to request a list of tools the server has. - */ -@Deprecated( - message = "Use `ListToolsRequest` instead", - replaceWith = ReplaceWith("ListToolsRequest", "io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest"), - level = DeprecationLevel.ERROR, -) -public typealias ListToolsRequest = io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest - -/** - * The server's response to a tools/list request from the client. - */ -@Deprecated( - message = "Use `ListToolsResult` instead", - replaceWith = ReplaceWith("ListToolsResult", "io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult"), - level = DeprecationLevel.ERROR, -) -public typealias ListToolsResult = io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult - -/** - * The server's response to a tool call. - */ -@Serializable -@Deprecated( - message = "This interface will be removed.", - replaceWith = ReplaceWith("CallToolResult", "io.modelcontextprotocol.kotlin.sdk.types.CallToolResult"), - level = DeprecationLevel.ERROR, -) -@Suppress("DEPRECATION_ERROR") -public sealed interface CallToolResultBase { - public val content: List - public val structuredContent: JsonObject? - public val isError: Boolean? get() = false -} - -@Deprecated( - message = "Use `CallToolResult` instead", - replaceWith = ReplaceWith("CallToolResult", "io.modelcontextprotocol.kotlin.sdk.types.CallToolResult"), - level = DeprecationLevel.ERROR, -) -public object CallToolResult - -/** - * The server's response to a tool call. - */ -@Deprecated( - message = "Use `CallToolResult` instead", - replaceWith = ReplaceWith("CallToolResult", "io.modelcontextprotocol.kotlin.sdk.types.CallToolResult"), - level = DeprecationLevel.ERROR, -) -public fun CallToolResult( - content: List, - isError: Boolean? = null, - structuredContent: JsonObject? = null, - meta: JsonObject? = null, -): io.modelcontextprotocol.kotlin.sdk.types.CallToolResult = - io.modelcontextprotocol.kotlin.sdk.types.CallToolResult(content, isError, structuredContent, meta) - -/** - * [CallToolResult] extended with backwards compatibility to protocol version 2024-11-05. - */ -@Serializable -@Deprecated( - message = "This class will be removed.", - replaceWith = ReplaceWith("ToolCallResult"), - level = DeprecationLevel.ERROR, -) -@Suppress("DEPRECATION_ERROR", "ConstructorParameterNaming") -public class CompatibilityCallToolResult( - public val content: List, - public val structuredContent: JsonObject? = null, - public val isError: Boolean? = false, - public val _meta: JsonObject = EmptyJsonObject, - public val toolResult: JsonObject = EmptyJsonObject, -) - -/** - * Used by the client to invoke a tool provided by the server. - */ -@Deprecated( - message = "Use `CallToolRequest` instead", - replaceWith = ReplaceWith("CallToolRequest", "io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest"), - level = DeprecationLevel.ERROR, -) -public typealias CallToolRequest = io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest - -@Deprecated( - message = "Use `ToolListChangedNotification` instead", - replaceWith = ReplaceWith( - "ToolListChangedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification", - ), - level = DeprecationLevel.ERROR, -) -public object ToolListChangedNotification { - @Deprecated( - message = "Use `BaseNotificationParams` instead", - replaceWith = ReplaceWith( - "BaseNotificationParams", - "io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams", - ), - level = DeprecationLevel.ERROR, - ) - public fun Params(meta: JsonObject? = null): io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams = - io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams(meta) -} - -/** - * An optional notification from the server to the client, informing it that the list of tools it offers has changed. - * Servers may issue this without any previous subscription from the client. - */ -@Deprecated( - message = "Use `ToolListChangedNotification` instead", - replaceWith = ReplaceWith( - "ToolListChangedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification", - ), - level = DeprecationLevel.ERROR, -) -public fun ToolListChangedNotification( - params: io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams, -): io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification = - io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification(params) - -/* Logging */ - -/** - * The severity of a log message. - */ -@Deprecated( - message = "Use `LoggingLevel` instead", - replaceWith = ReplaceWith("LoggingLevel", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), - level = DeprecationLevel.ERROR, -) -public object LoggingLevel { - @Deprecated( - message = "Use `LoggingLevel.Debug` instead", - replaceWith = ReplaceWith("LoggingLevel.Debug", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), - level = DeprecationLevel.ERROR, - ) - public val debug: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = - io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Debug - - @Deprecated( - message = "Use `LoggingLevel.Info` instead", - replaceWith = ReplaceWith("LoggingLevel.Info", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), - level = DeprecationLevel.ERROR, - ) - public val info: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = - io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Info - - @Deprecated( - message = "Use `LoggingLevel.Notice` instead", - replaceWith = ReplaceWith("LoggingLevel.Notice", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), - level = DeprecationLevel.ERROR, - ) - public val notice: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = - io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Notice - - @Deprecated( - message = "Use `LoggingLevel.Warning` instead", - replaceWith = ReplaceWith("LoggingLevel.Warning", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), - level = DeprecationLevel.ERROR, - ) - public val warning: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = - io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Warning - - @Deprecated( - message = "Use `LoggingLevel.Error` instead", - replaceWith = ReplaceWith("LoggingLevel.Error", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), - level = DeprecationLevel.ERROR, - ) - public val error: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = - io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Error - - @Deprecated( - message = "Use `LoggingLevel.Critical` instead", - replaceWith = ReplaceWith("LoggingLevel.Critical", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), - level = DeprecationLevel.ERROR, - ) - public val critical: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = - io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Critical - - @Deprecated( - message = "Use `LoggingLevel.Alert` instead", - replaceWith = ReplaceWith("LoggingLevel.Alert", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), - level = DeprecationLevel.ERROR, - ) - public val alert: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = - io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Alert - - @Deprecated( - message = "Use `LoggingLevel.Emergency` instead", - replaceWith = ReplaceWith("LoggingLevel.Emergency", "io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel"), - level = DeprecationLevel.ERROR, - ) - public val emergency: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel = - io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel.Emergency -} - -@Deprecated( - message = "Use `LoggingMessageNotification` instead", - replaceWith = ReplaceWith( - "LoggingMessageNotification", - "io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification", - ), - level = DeprecationLevel.ERROR, -) -public object LoggingMessageNotification { - @Deprecated( - message = "Use `LoggingMessageNotificationParams` instead", - replaceWith = ReplaceWith( - "LoggingMessageNotificationParams", - "io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams", - ), - level = DeprecationLevel.ERROR, - ) - public fun Params( - level: io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel, - data: JsonElement, - logger: String? = null, - meta: JsonObject? = null, - ): io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams = - io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams(level, data, logger, meta) - - @Deprecated( - message = "Use `SetLevelRequest` instead", - replaceWith = ReplaceWith( - "SetLevelRequest", - "io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest", - ), - level = DeprecationLevel.ERROR, - ) - public fun SetLevelRequest( - params: io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequestParams, - ): io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest = - io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest(params) -} - -/** - * Notification of a log message passed from server to client. - * If no logging level request has been sent from the client, - * the server MAY decide which messages to send automatically. - */ -@Deprecated( - message = "Use `LoggingMessageNotification` instead", - replaceWith = ReplaceWith( - "LoggingMessageNotification", - "io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification", - ), - level = DeprecationLevel.ERROR, -) -public fun LoggingMessageNotification( - params: io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams, -): io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification = - io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification(params) - -/* Sampling */ - -/** - * Hints to use for model selection. - */ -@Deprecated( - message = "Use `ModelHint` instead", - replaceWith = ReplaceWith("ModelHint", "io.modelcontextprotocol.kotlin.sdk.types.ModelHint"), - level = DeprecationLevel.ERROR, -) -public typealias ModelHint = io.modelcontextprotocol.kotlin.sdk.types.ModelHint - -/** - * The server's preferences for model selection, requested by the client during sampling. - */ -@Deprecated( - message = "Use `ModelPreferences` instead", - replaceWith = ReplaceWith("ModelPreferences", "io.modelcontextprotocol.kotlin.sdk.types.ModelPreferences"), - level = DeprecationLevel.ERROR, -) -public typealias ModelPreferences = io.modelcontextprotocol.kotlin.sdk.types.ModelPreferences - -/** - * Describes a message issued to or received from an LLM API. - */ -@Deprecated( - message = "Use `SamplingMessage` instead", - replaceWith = ReplaceWith("SamplingMessage", "io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage"), - level = DeprecationLevel.ERROR, -) -public typealias SamplingMessage = io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage - -@Deprecated( - message = "Use `CreateMessageRequest` instead", - replaceWith = ReplaceWith("CreateMessageRequest", "io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest"), - level = DeprecationLevel.ERROR, -) -public object CreateMessageRequest { - public val IncludeContext: io.modelcontextprotocol.kotlin.sdk.types.IncludeContext.Companion = - io.modelcontextprotocol.kotlin.sdk.types.IncludeContext -} - -/** - * A request from the server to sample an LLM via the client. - * The client has full discretion over which model to select. - * The client should also inform the user before beginning sampling to allow them to inspect the request - * (human in the loop) and decide whether to approve it. - */ -@Deprecated( - message = "Use `CreateMessageRequest` instead", - replaceWith = ReplaceWith("CreateMessageRequest", "io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest"), - level = DeprecationLevel.ERROR, -) -public fun CreateMessageRequest( - params: io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams, -): io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest = - io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest(params) - -@Deprecated( - message = "Use `StopReason` instead", - replaceWith = ReplaceWith("StopReason", "io.modelcontextprotocol.kotlin.sdk.types.StopReason"), - level = DeprecationLevel.ERROR, -) -public typealias StopReason = io.modelcontextprotocol.kotlin.sdk.types.StopReason - -/** - * The client's response to a sampling/create_message request from the server. - * The client should inform the user before returning the sampled message to allow them to inspect the response - * (human in the loop) and decide whether to allow the server to see it. - */ -@Deprecated( - message = "Use `CreateMessageResult` instead", - replaceWith = ReplaceWith("CreateMessageResult", "io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult"), - level = DeprecationLevel.ERROR, -) -public typealias CreateMessageResult = io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult - -/* Autocomplete */ -@Deprecated( - message = "Use `Reference` instead", - replaceWith = ReplaceWith("Reference", "io.modelcontextprotocol.kotlin.sdk.types.Reference"), - level = DeprecationLevel.ERROR, -) -public typealias Reference = io.modelcontextprotocol.kotlin.sdk.types.Reference - -/** - * A reference to a resource or resource template definition. - */ -@Deprecated( - message = "Use `ResourceTemplateReference` instead", - replaceWith = ReplaceWith( - "ResourceTemplateReference", - "io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplateReference", - ), - level = DeprecationLevel.ERROR, -) -public typealias ResourceTemplateReference = io.modelcontextprotocol.kotlin.sdk.types.ResourceTemplateReference - -/** - * Identifies a prompt. - */ -@Deprecated( - message = "Use `PromptReference` instead", - replaceWith = ReplaceWith("PromptReference", "io.modelcontextprotocol.kotlin.sdk.types.PromptReference"), - level = DeprecationLevel.ERROR, -) -public typealias PromptReference = io.modelcontextprotocol.kotlin.sdk.types.PromptReference - -/** - * Identifies a prompt. - */ -@Serializable -@Deprecated( - message = "This class will be removed.", - level = DeprecationLevel.ERROR, -) -public data class UnknownReference(val type: String) - -@Deprecated( - message = "Use `CompleteRequest` instead", - replaceWith = ReplaceWith("CompleteRequest", "io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest"), - level = DeprecationLevel.ERROR, -) -public object CompleteRequest { - @Deprecated( - message = "Use `CompleteRequestParams.Argument` instead", - replaceWith = ReplaceWith( - "CompleteRequestParams.Argument", - "io.modelcontextprotocol.kotlin.sdk.types.CompleteRequestParams.Argument", - ), - level = DeprecationLevel.ERROR, - ) - public fun Argument( - name: String, - value: String, - ): io.modelcontextprotocol.kotlin.sdk.types.CompleteRequestParams.Argument = - io.modelcontextprotocol.kotlin.sdk.types.CompleteRequestParams.Argument(name, value) -} - -/** - * A request from the client to the server to ask for completion options. - */ -@Deprecated( - message = "Use `CompleteRequest` instead", - replaceWith = ReplaceWith("CompleteRequest", "io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest"), - level = DeprecationLevel.ERROR, -) -public fun CompleteRequest( - params: io.modelcontextprotocol.kotlin.sdk.types.CompleteRequestParams, -): io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest = - io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest(params) - -@Deprecated( - message = "Use `CompleteResult` instead", - replaceWith = ReplaceWith("CompleteResult", "io.modelcontextprotocol.kotlin.sdk.types.CompleteResult"), - level = DeprecationLevel.ERROR, -) -public object CompleteResult { - @Deprecated( - message = "Use `CompleteResult.Completion` instead", - replaceWith = ReplaceWith( - "CompleteResult.Completion", - "io.modelcontextprotocol.kotlin.sdk.types.CompleteResult.Completion", - ), - level = DeprecationLevel.ERROR, - ) - public fun Completion( - values: List, - total: Int? = null, - hasMore: Boolean? = null, - ): io.modelcontextprotocol.kotlin.sdk.types.CompleteResult.Completion = - io.modelcontextprotocol.kotlin.sdk.types.CompleteResult.Completion(values, total, hasMore) -} - -/** - * The server's response to a completion/complete request - */ -@Deprecated( - message = "Use `CompleteResult` instead", - replaceWith = ReplaceWith("CompleteResult", "io.modelcontextprotocol.kotlin.sdk.types.CompleteResult"), - level = DeprecationLevel.ERROR, -) -public fun CompleteResult( - completion: io.modelcontextprotocol.kotlin.sdk.types.CompleteResult.Completion, - meta: JsonObject? = null, -): io.modelcontextprotocol.kotlin.sdk.types.CompleteResult = - io.modelcontextprotocol.kotlin.sdk.types.CompleteResult(completion, meta) - -/* Roots */ - -/** - * Represents a root directory or file that the server can operate on. - */ -@Deprecated( - message = "Use `Root` instead", - replaceWith = ReplaceWith("Root", "io.modelcontextprotocol.kotlin.sdk.types.Root"), - level = DeprecationLevel.ERROR, -) -public typealias Root = io.modelcontextprotocol.kotlin.sdk.types.Root - -/** - * Sent from the server to request a list of root URIs from the client. - */ -@Deprecated( - message = "Use `ListRootsRequest` instead", - replaceWith = ReplaceWith("ListRootsRequest", "io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest"), - level = DeprecationLevel.ERROR, -) -public typealias ListRootsRequest = io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest - -/** - * The client's response to a roots/list request from the server. - */ -@Deprecated( - message = "Use `ListRootsResult` instead", - replaceWith = ReplaceWith("ListRootsResult", "io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult"), - level = DeprecationLevel.ERROR, -) -public typealias ListRootsResult = io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult - -@Deprecated( - message = "Use `RootsListChangedNotification` instead", - replaceWith = ReplaceWith( - "RootsListChangedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification", - ), - level = DeprecationLevel.ERROR, -) -public object RootsListChangedNotification { - @Deprecated( - message = "Use `BaseNotificationParams` instead", - replaceWith = ReplaceWith( - "BaseNotificationParams", - "io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams", - ), - level = DeprecationLevel.ERROR, - ) - public fun Params(meta: JsonObject? = null): io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams = - io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams(meta) -} - -/** - * A notification from the client to the server, informing it that the list of roots has changed. - */ -@Deprecated( - message = "Use `RootsListChangedNotification` instead", - replaceWith = ReplaceWith( - "RootsListChangedNotification", - "io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification", - ), - level = DeprecationLevel.ERROR, -) -public fun RootsListChangedNotification( - params: io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams? = null, -): io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification = - io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification(params) - -@Deprecated( - message = "Use `ElicitRequest` instead", - replaceWith = ReplaceWith( - "ElicitRequest", - "io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest", - ), - level = DeprecationLevel.ERROR, -) -public object CreateElicitationRequest { - public fun RequestedSchema( - properties: JsonObject, - required: List? = null, - ): io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams.RequestedSchema = - io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams.RequestedSchema(properties, required) -} - -/** - * Sent from the server to create an elicitation from the client. - */ -@Deprecated( - message = "Use `ElicitRequest` instead", - replaceWith = ReplaceWith( - "ElicitRequest", - "io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest", - ), - level = DeprecationLevel.ERROR, -) -public fun CreateElicitationRequest( - params: io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams, -): io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest = - io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest(params) - -@Deprecated( - message = "Use `ElicitResult` instead", - replaceWith = ReplaceWith( - "ElicitResult", - "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult", - ), - level = DeprecationLevel.ERROR, -) -public object CreateElicitationResult { - @Deprecated( - message = "Use `ElicitResult.Action` instead", - replaceWith = ReplaceWith( - "ElicitResult.Action", - "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action", - ), - level = DeprecationLevel.ERROR, - ) - public object Action { - @Deprecated( - message = "Use `ElicitResult.Action.Accept` instead", - replaceWith = ReplaceWith( - "ElicitResult.Action.Accept", - "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action", - ), - level = DeprecationLevel.ERROR, - ) - public val accept: io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action = - io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action.Accept - - @Deprecated( - message = "Use `ElicitResult.Action.Decline` instead", - replaceWith = ReplaceWith( - "ElicitResult.Action.Decline", - "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action", - ), - level = DeprecationLevel.ERROR, - ) - public val decline: io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action = - io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action.Decline - - @Deprecated( - message = "Use `ElicitResult.Action.Cancel` instead", - replaceWith = ReplaceWith( - "ElicitResult.Action.Cancel", - "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action", - ), - level = DeprecationLevel.ERROR, - ) - public val cancel: io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action = - io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action.Cancel - } -} - -/** - * The client's response to an elicitation/create request from the server. - */ -@Deprecated( - message = "Use `ElicitResult` instead", - replaceWith = ReplaceWith( - "ElicitResult", - "io.modelcontextprotocol.kotlin.sdk.types.ElicitResult", - ), - level = DeprecationLevel.ERROR, -) -public fun CreateElicitationResult( - action: io.modelcontextprotocol.kotlin.sdk.types.ElicitResult.Action, - content: JsonObject? = null, - meta: JsonObject? = null, -): io.modelcontextprotocol.kotlin.sdk.types.ElicitResult = - io.modelcontextprotocol.kotlin.sdk.types.ElicitResult(action, content, meta) - -/** - * Represents an error specific to the MCP protocol. - */ -@Deprecated( - message = "Use `McpException` instead", - replaceWith = ReplaceWith("McpException"), - level = DeprecationLevel.ERROR, -) -public typealias McpError = io.modelcontextprotocol.kotlin.sdk.types.McpException 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 deleted file mode 100644 index 3e0abe10f..000000000 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk - -import io.modelcontextprotocol.kotlin.sdk.types.error -import io.modelcontextprotocol.kotlin.sdk.types.success -import kotlinx.serialization.json.JsonObject - -@Deprecated( - message = "Use `io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject` instead", - replaceWith = ReplaceWith("EmptyJsonObject", "io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject"), - level = DeprecationLevel.ERROR, -) -public val EmptyJsonObject: JsonObject = io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject - -/** - * Creates a [CallToolResult] with single [TextContent] and [meta]. - */ -@Deprecated( - message = "Use `CallToolResult.success` instead", - replaceWith = ReplaceWith("CallToolResult.Companion.success"), - level = DeprecationLevel.ERROR, -) -@Suppress("DEPRECATION_ERROR") -public fun CallToolResult.ok( - content: String, - meta: JsonObject = EmptyJsonObject, -): io.modelcontextprotocol.kotlin.sdk.types.CallToolResult = - io.modelcontextprotocol.kotlin.sdk.types.CallToolResult.success(content, meta) - -/** - * Creates a [CallToolResult] with single [TextContent] and [meta], with `isError` being true. - */ -@Deprecated( - message = "Use `CallToolResult.error` instead", - replaceWith = ReplaceWith("CallToolResult.Companion.error"), - level = DeprecationLevel.ERROR, -) -@Suppress("DEPRECATION_ERROR") -public fun CallToolResult.error( - content: String, - meta: JsonObject = EmptyJsonObject, -): io.modelcontextprotocol.kotlin.sdk.types.CallToolResult = - io.modelcontextprotocol.kotlin.sdk.types.CallToolResult.error(content, meta) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt index 8ddee0a9c..7ec9c076e 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt @@ -30,15 +30,6 @@ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( public sealed interface WithMeta { @SerialName("_meta") public val meta: JsonObject? - - @Deprecated( - message = "Use 'meta' instead.", - replaceWith = ReplaceWith("meta"), - level = DeprecationLevel.ERROR, - ) - @Suppress("PropertyName", "VariableNaming") - public val _meta: JsonObject - get() = meta ?: EmptyJsonObject } // ============================================================================ diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt index 5ca41c6fa..79a76ea53 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt @@ -39,18 +39,6 @@ public data class CompleteRequest(override val params: CompleteRequestParams) : public val meta: RequestMeta? get() = params.meta - - @Deprecated( - message = "Will be removed in a future release.", - replaceWith = ReplaceWith(expression = "CompleteRequest(CompleteRequestParams(argument, ref, context, meta))"), - level = DeprecationLevel.ERROR, - ) - public constructor( - argument: Argument, - ref: Reference, - context: Context? = null, - meta: RequestMeta? = null, - ) : this(CompleteRequestParams(argument, ref, context, meta)) } /** diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.kt index 08a7a9fad..5d7054b73 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/prompts.kt @@ -113,23 +113,6 @@ public data class GetPromptRequest(override val params: GetPromptRequestParams) @EncodeDefault override val method: Method = Method.Defined.PromptsGet - @Deprecated( - message = "Use constructor with GetPromptRequestParams instead", - replaceWith = ReplaceWith("GetPromptRequest(GetPromptRequestParams(name, arguments, meta))"), - level = DeprecationLevel.ERROR, - ) - public constructor( - name: String, - arguments: Map? = null, - meta: RequestMeta? = null, - ) : this( - GetPromptRequestParams( - name = name, - arguments = arguments, - meta = meta, - ), - ) - /** * The name of the prompt or prompt template to retrieve. */ @@ -204,26 +187,6 @@ public data class ListPromptsRequest(override val params: PaginatedRequestParams PaginatedRequest { @EncodeDefault override val method: Method = Method.Defined.PromptsList - - /** - * Secondary constructor for creating a [ListPromptsRequest] instance - * using optional cursor and metadata parameters. - * - * This constructor simplifies the creation of the [ListPromptsRequest] by allowing a cursor - * and metadata to be provided. - * - * @param cursor Optional cursor string to specify the starting point of the paginated request. - * @param meta Optional metadata associated with the request. - */ - @Deprecated( - message = "Use constructor with ListPromptsParams instead", - replaceWith = ReplaceWith("ListPromptsRequest(ListPromptsParams(name, arguments, meta))"), - level = DeprecationLevel.ERROR, - ) - public constructor( - cursor: String?, - meta: RequestMeta? = null, - ) : this(paginatedRequestParams(cursor, meta)) } /** diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt index 8782db1bd..fa316dec5 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt @@ -191,26 +191,6 @@ public data class ListResourcesRequest(override val params: PaginatedRequestPara PaginatedRequest { @EncodeDefault override val method: Method = Method.Defined.ResourcesList - - /** - * Secondary constructor for creating a [ListResourcesRequest] instance - * using optional cursor and metadata parameters. - * - * This constructor simplifies the creation of the [ListResourcesRequest] by allowing a cursor - * and metadata to be provided. - * - * @param cursor Optional cursor string to specify the starting point of the paginated request. - * @param meta Optional metadata associated with the request. - */ - @Deprecated( - message = "Use the constructor with PaginatedRequestParams property instead", - replaceWith = ReplaceWith("ReadResourceRequest(PaginatedRequestParams(cursor, meta))"), - level = DeprecationLevel.ERROR, - ) - public constructor( - cursor: String?, - meta: RequestMeta? = null, - ) : this(paginatedRequestParams(cursor, meta)) } /** @@ -261,16 +241,6 @@ public data class ReadResourceRequest(override val params: ReadResourceRequestPa */ public val meta: RequestMeta? get() = params.meta - - @Deprecated( - message = "Use the constructor with ReadResourceRequestParams property instead", - replaceWith = ReplaceWith("ReadResourceRequest(ReadResourceRequestParams(uri, meta))"), - level = DeprecationLevel.ERROR, - ) - public constructor( - uri: String, - meta: RequestMeta? = null, - ) : this(ReadResourceRequestParams(uri, meta)) } /** @@ -337,16 +307,6 @@ public data class SubscribeRequest(override val params: SubscribeRequestParams) */ public val meta: RequestMeta? get() = params.meta - - @Deprecated( - message = "Use the constructor with SubscribeRequestParams property instead", - replaceWith = ReplaceWith("ReadResourceRequest(SubscribeRequestParams(uri, meta))"), - level = DeprecationLevel.ERROR, - ) - public constructor( - uri: String, - meta: RequestMeta? = null, - ) : this(SubscribeRequestParams(uri, meta)) } /** @@ -433,26 +393,6 @@ public data class ListResourceTemplatesRequest(override val params: PaginatedReq PaginatedRequest { @EncodeDefault override val method: Method = Method.Defined.ResourcesTemplatesList - - /** - * Secondary constructor for creating a [ListResourceTemplatesRequest] instance - * using optional cursor and metadata parameters. - * - * This constructor simplifies the creation of the [ListResourceTemplatesRequest] by allowing a cursor - * and metadata to be provided. - * - * @param cursor Optional cursor string to specify the starting point of the paginated request. - * @param meta Optional metadata associated with the request. - */ - @Deprecated( - message = "Use the constructor with PaginatedRequestParams property instead", - replaceWith = ReplaceWith("ListResourceTemplatesRequest(PaginatedRequestParams(cursor, meta))"), - level = DeprecationLevel.ERROR, - ) - public constructor( - cursor: String?, - meta: RequestMeta? = null, - ) : this(paginatedRequestParams(cursor, meta)) } /** diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt index 9e7dbae86..af7b1e7a2 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt @@ -62,15 +62,6 @@ public data class ListRootsRequest(override val params: BaseRequestParams? = nul public val meta: RequestMeta? get() = params?.meta - - @Deprecated( - message = "Use the constructor with BaseRequestParams property instead", - replaceWith = ReplaceWith("ListRootsRequest(BaseRequestParams(meta))"), - level = DeprecationLevel.ERROR, - ) - public constructor( - meta: RequestMeta?, - ) : this(BaseRequestParams(meta)) } /** diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt index c121f9d27..4f642f44a 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt @@ -267,26 +267,6 @@ public data class ListToolsRequest(override val params: PaginatedRequestParams? PaginatedRequest { @EncodeDefault override val method: Method = Method.Defined.ToolsList - - /** - * Secondary constructor for creating a [ListToolsRequest] instance - * using optional cursor and metadata parameters. - * - * This constructor simplifies the creation of the [ListToolsRequest] by allowing a cursor - * and metadata to be provided. - * - * @param cursor Optional cursor string to specify the starting point of the paginated request. - * @param meta Optional metadata associated with the request. - */ - @Deprecated( - message = "Use the constructor with BaseRequestParams property instead", - replaceWith = ReplaceWith("ListToolsRequest(BaseRequestParams(meta))"), - level = DeprecationLevel.ERROR, - ) - public constructor( - cursor: String?, - meta: RequestMeta? = null, - ) : this(paginatedRequestParams(cursor, meta)) } /** diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index b1b898a33..fef34b261 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -107,7 +107,6 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { public final fun addTools (Ljava/util/List;)V public final fun clientConnection (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection; public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun createElicitation (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$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;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun createMessage (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -125,7 +124,6 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun onClose (Lkotlin/jvm/functions/Function0;)V public final fun onConnect (Lkotlin/jvm/functions/Function0;)V - public final fun onInitialized (Lkotlin/jvm/functions/Function0;)V public final fun ping (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun removeNotificationHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V public final fun removeNotificationHandler (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index e221482cb..afed86a34 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -118,8 +118,6 @@ public open class Server( block: Server.() -> Unit = {}, ) : this(serverInfo, options, { instructions }, block) - private var _onInitialized: (() -> Unit) = {} - private var _onConnect: (() -> Unit) = {} private var _onClose: () -> Unit = {} @@ -197,20 +195,6 @@ public open class Server( _onClose() } - /** - * Starts a new server session with the given transport and initializes - * internal request handlers based on the server's capabilities. - * - * @param transport The transport layer to connect the session with. - * @return The initialized and connected server session. - */ - @Deprecated( - "Use createSession(transport) instead.", - ReplaceWith("createSession(transport)"), - DeprecationLevel.ERROR, - ) - public suspend fun connect(transport: Transport): ServerSession = createSession(transport) - /** * Starts a new server session with the given transport and initializes * internal request handlers based on the server's capabilities. @@ -294,22 +278,6 @@ public open class Server( } } - /** - * Registers a callback to be invoked when the server has completed initialization. - */ - @Deprecated( - "Initialization moved to ServerSession, use ServerSession.onInitialized instead.", - ReplaceWith("ServerSession.onInitialized"), - DeprecationLevel.ERROR, - ) - public fun onInitialized(block: () -> Unit) { - val old = _onInitialized - _onInitialized = { - old() - block() - } - } - /** * Registers a callback to be invoked when the server connection is closing. */ From 435698a611b106efcf5dfa737b4291e5036edfff Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Thu, 26 Mar 2026 13:19:10 +0100 Subject: [PATCH 034/187] refactor(integration-test): modernize ts tests, update versions, rename SSE -> HTTP (#618) - add tsconfig - fix tsignore - rename sse -> http - clean up code ## How Has This Been Tested? all tests passed ## Breaking Changes none ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .../main/kotlin/mcp.multiplatform.gradle.kts | 1 - config/detekt/detekt.yml | 4 + integration-test/detekt-baseline-test.xml | 14 -- .../integration/AbstractAuthenticationTest.kt | 1 - ...tractKotlinClientTsServerEdgeCasesTest.kt} | 45 ++--- .../sdk/integration/typescript/TsTestBase.kt | 26 +-- .../typescript/TypeScriptClient.kt | 10 +- .../typescript/TypeScriptServer.kt | 20 +-- .../KotlinClientTsServerEdgeCasesTestHttp.kt | 14 ++ .../KotlinClientTsServerTestHttp.kt} | 9 +- .../KotlinServerForTsClientHttp.kt} | 48 ++--- .../TsClientKotlinServerTestHttp.kt} | 10 +- .../TsEdgeCasesTestHttp.kt} | 26 +-- .../KotlinClientTsServerEdgeCasesTestStdio.kt | 167 +----------------- .../client/{sse-client.ts => http-client.ts} | 39 ++-- .../jvmTest/typescript/client/stdio-client.ts | 21 +-- .../src/jvmTest/typescript/package-lock.json | 71 ++------ .../src/jvmTest/typescript/package.json | 10 +- .../server/{sse-server.ts => http-server.ts} | 55 ++---- .../src/jvmTest/typescript/tsconfig.json | 12 ++ kotlin-sdk-client/detekt-baseline-test.xml | 2 - .../kotlin/test/utils/testing.kt | 1 - 22 files changed, 169 insertions(+), 437 deletions(-) rename integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/{sse/KotlinClientTsServerEdgeCasesTestSse.kt => AbstractKotlinClientTsServerEdgeCasesTest.kt} (82%) create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerEdgeCasesTestHttp.kt rename integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/{sse/KotlinClientTsServerTestSse.kt => http/KotlinClientTsServerTestHttp.kt} (75%) rename integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/{sse/KotlinServerForTsClientSse.kt => http/KotlinServerForTsClientHttp.kt} (92%) rename integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/{sse/TsClientKotlinServerTestSse.kt => http/TsClientKotlinServerTestHttp.kt} (85%) rename integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/{sse/TsEdgeCasesTestSse.kt => http/TsEdgeCasesTestHttp.kt} (90%) rename integration-test/src/jvmTest/typescript/client/{sse-client.ts => http-client.ts} (67%) rename integration-test/src/jvmTest/typescript/server/{sse-server.ts => http-server.ts} (80%) create mode 100644 integration-test/src/jvmTest/typescript/tsconfig.json diff --git a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts index f3d4616ff..36097ffa0 100644 --- a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts +++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts @@ -39,7 +39,6 @@ kotlin { forkEvery = 100 testLogging { exceptionFormat = TestExceptionFormat.SHORT - showStandardStreams = true events("failed") } systemProperty("kotest.output.ansi", "true") diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 8c19749c2..2cb135abb 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -14,6 +14,10 @@ complexity: LongMethod: excludes: *testFolders +coroutines: + InjectDispatcher: + excludes: *testFolders + empty-blocks: EmptyFunctionBlock: excludes: *testFolders diff --git a/integration-test/detekt-baseline-test.xml b/integration-test/detekt-baseline-test.xml index 710b72936..2df611a62 100644 --- a/integration-test/detekt-baseline-test.xml +++ b/integration-test/detekt-baseline-test.xml @@ -8,24 +8,10 @@ AbstractClassCanBeConcreteClass:BaseTransportTest.kt:BaseTransportTest$BaseTransportTest CyclomaticComplexMethod:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$private fun setupCalculatorTool ForbiddenComment:StdioClientTransportTest.kt:StdioClientTransportTest$// TODO: fix running on windows - InjectDispatcher:AbstractKotlinClientTsServerTest.kt:AbstractKotlinClientTsServerTest$IO - InjectDispatcher:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$IO - InjectDispatcher:AbstractResourceIntegrationTest.kt:AbstractResourceIntegrationTest$IO - InjectDispatcher:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$IO - InjectDispatcher:KotlinClientTsServerEdgeCasesTestSse.kt:KotlinClientTsServerEdgeCasesTestSse$IO - InjectDispatcher:KotlinClientTsServerEdgeCasesTestStdio.kt:KotlinClientTsServerEdgeCasesTestStdio$IO - InjectDispatcher:SseIntegrationTest.kt:SseIntegrationTest$IO - InjectDispatcher:StdioClientTransportTest.kt:StdioClientTransportTest$IO - InjectDispatcher:StreamableHttpIntegrationTest.kt:StreamableHttpIntegrationTest$IO - InjectDispatcher:TsEdgeCasesTestSse.kt:TsEdgeCasesTestSse$IO - InjectDispatcher:WebSocketIntegrationTest.kt:WebSocketIntegrationTest$IO MatchingDeclarationName:PromptIntegrationTestSse.kt:SchemaPromptIntegrationTestSse : AbstractPromptIntegrationTest - SleepInsteadOfDelay:KotlinServerForTsClientSse.kt:KotlinServerForTsClient$sleep(500) ThrowsCount:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$override fun configureServer - UnusedPrivateProperty:KotlinClientTsServerTestSse.kt:KotlinClientTsServerTestSse$private val host = "localhost" UseCheckOrError:ClientTest.kt:ClientTest.<no name provided>$throw IllegalStateException("Test error") UseOrEmpty:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$(request.params.arguments?.get("tags") as? JsonArray)?.mapNotNull { (it as? JsonPrimitive)?.content } ?: emptyList() UseRequire:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$throw IllegalArgumentException("Missing required argument: $argName") - VarCouldBeVal:KotlinClientTsServerEdgeCasesTestSse.kt:KotlinClientTsServerEdgeCasesTestSse$private lateinit var client: Client
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt index 1e88558fa..ec4204145 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/AbstractAuthenticationTest.kt @@ -42,7 +42,6 @@ import io.ktor.server.sse.SSE as ServerSSE /** * Base class for MCP authentication integration tests. */ -@Suppress("InjectDispatcher") abstract class AbstractAuthenticationTest { protected companion object { diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerEdgeCasesTest.kt similarity index 82% rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerEdgeCasesTest.kt index 356fd3070..d977bd09f 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerEdgeCasesTest.kt @@ -1,53 +1,29 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse +package io.modelcontextprotocol.kotlin.sdk.integration.typescript import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive -import org.junit.jupiter.api.AfterEach 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 KotlinClientTsServerEdgeCasesTestSse : TsTestBase() { +abstract class AbstractKotlinClientTsServerEdgeCasesTest : TsTestBase() { - override val transportKind = TransportKind.SSE - - private val serverUrl: String by lazy { getSharedSseUrl() } - - private lateinit var client: Client - - @AfterEach - fun tearDown() { - if (::client.isInitialized) { - try { - runBlocking { - withTimeout(3.seconds) { - client.close() - } - } - } catch (e: Exception) { - println("Warning: Error during client close: ${e.message}") - } - } - } + protected abstract suspend fun useClient(block: suspend (Client) -> T): T @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testNonExistentTool(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> + useClient { client -> val nonExistentToolName = "non-existent-tool" val arguments = mapOf("name" to "TestUser") @@ -70,7 +46,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testSpecialCharactersInArguments(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> + useClient { client -> val specialChars = "!@#$%^&*()_+{}[]|\\:;\"'<>,.?/" val arguments = mapOf("name" to specialChars) @@ -91,7 +67,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testLargePayload(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> + useClient { client -> val largeName = "A".repeat(10 * 1024) val arguments = mapOf("name" to largeName) @@ -112,7 +88,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() { @Test @Timeout(60, unit = TimeUnit.SECONDS) fun testConcurrentRequests(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> + useClient { client -> val concurrentCount = 5 val responses = coroutineScope { val results = (1..concurrentCount).map { i -> @@ -147,7 +123,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testInvalidArguments(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> + useClient { client -> val invalidArguments = mapOf( "name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))), ) @@ -171,7 +147,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testMultipleToolCalls(): Unit = runBlocking(Dispatchers.IO) { - withClient(serverUrl) { client -> + useClient { client -> repeat(10) { i -> val name = "SequentialClient$i" val arguments = mapOf("name" to name) @@ -179,8 +155,7 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() { val result = client.callTool("greet", arguments) assertNotNull(result, "Tool call result should not be null for call $i") - val callResult = result - val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent + val textContent = result.content.firstOrNull { it is TextContent } as? TextContent assertNotNull(textContent, "Text content should be present for call $i") assertEquals( diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt index aa47a708f..29a6de734 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TsTestBase.kt @@ -6,7 +6,7 @@ import io.ktor.client.plugins.sse.SSE import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse.KotlinServerForTsClient +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.http.KotlinServerForTsClient import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport import io.modelcontextprotocol.kotlin.sdk.types.Implementation @@ -26,7 +26,7 @@ import java.util.concurrent.TimeUnit import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -enum class TransportKind { SSE, STDIO } +enum class TransportKind { HTTP, STDIO } @Retry(times = 3) abstract class TsTestBase { @@ -43,10 +43,10 @@ abstract class TsTestBase { protected val typescriptDir = File(projectRoot, "src/jvmTest/typescript") @JvmStatic - private var sharedSseServer: Process? = null + private var sharedHttpServer: Process? = null @JvmStatic - private var sharedSsePort: Int = 0 + private var sharedHttpPort: Int = 0 init { installDependencies(typescriptDir) @@ -54,23 +54,23 @@ abstract class TsTestBase { @JvmStatic @Synchronized - protected fun getSharedSseUrl(): String { - if (sharedSseServer == null || !sharedSseServer!!.isAlive) { - sharedSsePort = io.modelcontextprotocol.kotlin.test.utils.findFreePort() + protected fun getSharedHttpUrl(): String { + if (sharedHttpServer == null || !sharedHttpServer!!.isAlive) { + sharedHttpPort = io.modelcontextprotocol.kotlin.test.utils.findFreePort() val server = TypeScriptServer(typescriptDir) - sharedSseServer = server.startSse(sharedSsePort) - println("Shared TypeScript SSE server started on port $sharedSsePort") + sharedHttpServer = server.startHttp(sharedHttpPort) + println("Shared TypeScript HTTP server started on port $sharedHttpPort") Runtime.getRuntime().addShutdownHook( Thread { - sharedSseServer?.let { - println("Stopping shared TypeScript SSE server") + sharedHttpServer?.let { + println("Stopping shared TypeScript HTTP server") io.modelcontextprotocol.kotlin.test.utils.stopProcess(it) } }, ) } - return "http://localhost:$sharedSsePort/mcp" + return "http://localhost:$sharedHttpPort/mcp" } @JvmStatic @@ -141,7 +141,7 @@ abstract class TsTestBase { println("$name stopped") } - // ===== SSE client helpers ===== + // ===== HTTP client helpers ===== protected suspend fun newClient(serverUrl: String): Client = HttpClient(CIO) { install(SSE) }.mcpStreamableHttp(serverUrl) diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClient.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClient.kt index 7664abf22..109eab4a5 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClient.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClient.kt @@ -7,28 +7,28 @@ import java.io.File /** * Component for starting and managing TypeScript MCP clients for integration tests. * - * This class provides methods to start TypeScript clients using SSE or STDIO transports. + * This class provides methods to start TypeScript clients using HTTP or STDIO transports. * The clients are started using npx tsx to execute TypeScript files directly. */ class TypeScriptClient(private val typescriptDir: File) : AutoCloseable { private var process: Process? = null /** - * Starts a TypeScript MCP client using SSE transport. + * Starts a TypeScript MCP client using Streamable HTTP transport. * * @param arguments Arguments to pass to the client (typically server URL, tool name, and tool args) * @param log Whether to automatically log process output/error * @return The started Process */ - fun startSse(arguments: List, log: Boolean = true): Process { - val scriptPath = File(typescriptDir, "client/sse-client.ts").absolutePath + fun startHttp(arguments: List, log: Boolean = true): Process { + val scriptPath = File(typescriptDir, "client/http-client.ts").absolutePath val proc = TypeScriptRunner.run( typescriptDir = typescriptDir, scriptPath = scriptPath, arguments = arguments, env = emptyMap(), redirectErrorStream = false, - logPrefix = "TS-CLIENT-SSE", + logPrefix = "TS-CLIENT-HTTP", log = log, ) process = proc diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptServer.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptServer.kt index 579f96d43..411a2128e 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptServer.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptServer.kt @@ -12,23 +12,23 @@ import java.util.concurrent.TimeUnit * Factory class for starting and managing TypeScript MCP servers for integration tests. * * This class provides methods to: - * - Start TypeScript servers using SSE or STDIO transports via npx tsx + * - Start TypeScript servers using HTTP or STDIO transports via npx tsx * - Install npm dependencies in the TypeScript test directory * - Manage server lifecycle (start/stop) * - Handle port management and process cleanup * * The servers are started using npx tsx to execute TypeScript files directly: * - `npx tsx server/stdio-server.ts` for STDIO transport - * - `npx tsx server/sse-server.ts` for SSE transport + * - `npx tsx server/http-server.ts` for HTTP transport * * Usage: * ```kotlin * val typescriptDir = File("kotlin-sdk-test/src/jvmTest/typescript") * val server = TypeScriptServer(typescriptDir) * - * // Start SSE server + * // Start HTTP server * val port = TypeScriptServer.findFreePort() - * val process = server.startSse(port) + * val process = server.startHttp(port) * * // Stop server * server.stop() @@ -38,27 +38,27 @@ class TypeScriptServer(private val typescriptDir: File) { private var process: Process? = null /** - * Starts a TypeScript MCP server using SSE transport. + * Starts a TypeScript MCP server using Streamable HTTP transport. * - * @param port The port number to bind the SSE server to + * @param port The port number to bind the HTTP server to * @return The started Process * @throws IllegalStateException if the server fails to start within timeout */ - fun startSse(port: Int): Process { + fun startHttp(port: Int): Process { killProcessOnPort(port) - val serverPath = File(typescriptDir, "server/sse-server.ts").absolutePath + val serverPath = File(typescriptDir, "server/http-server.ts").absolutePath val proc = TypeScriptRunner.run( typescriptDir = typescriptDir, scriptPath = serverPath, env = mapOf("MCP_PORT" to port.toString()), redirectErrorStream = true, - logPrefix = "TS-SERVER-SSE", + logPrefix = "TS-SERVER-HTTP", ) process = proc check(waitForPort(port = port, timeoutSeconds = 60)) { proc.destroyForcibly() - "TypeScript SSE server did not become ready on localhost:$port within timeout" + "TypeScript HTTP server did not become ready on localhost:$port within timeout" } return proc } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerEdgeCasesTestHttp.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerEdgeCasesTestHttp.kt new file mode 100644 index 000000000..016aa1cb5 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerEdgeCasesTestHttp.kt @@ -0,0 +1,14 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http + +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerEdgeCasesTest +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind + +class KotlinClientTsServerEdgeCasesTestHttp : AbstractKotlinClientTsServerEdgeCasesTest() { + + override val transportKind = TransportKind.HTTP + + private val serverUrl: String by lazy { getSharedHttpUrl() } + + override suspend fun useClient(block: suspend (Client) -> T): T = withClient(serverUrl, block) +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerTestHttp.kt similarity index 75% rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerTestHttp.kt index 200628500..7d919af2f 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinClientTsServerTestHttp.kt @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerTest @@ -6,11 +6,10 @@ import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind import kotlinx.coroutines.withTimeout import kotlin.time.Duration.Companion.seconds -class KotlinClientTsServerTestSse : AbstractKotlinClientTsServerTest() { - override val transportKind = TransportKind.SSE +class KotlinClientTsServerTestHttp : AbstractKotlinClientTsServerTest() { + override val transportKind = TransportKind.HTTP - private val host = "localhost" - private val serverUrl: String by lazy { getSharedSseUrl() } + private val serverUrl: String by lazy { getSharedHttpUrl() } override suspend fun useClient(block: suspend (Client) -> T): T = withClient(serverUrl) { client -> try { diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinServerForTsClientHttp.kt similarity index 92% rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinServerForTsClientHttp.kt index 97acc29f6..ebd049985 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/KotlinServerForTsClientHttp.kt @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.http.ContentType @@ -42,8 +42,9 @@ import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement @@ -132,14 +133,16 @@ class KotlinServerForTsClient { call.response.header("mcp-session-id", newSessionId) - val serverThread = Thread { - runBlocking { + launch(Dispatchers.IO) { + try { mcpServer.createSession(transport) + } catch (e: Exception) { + logger.error(e) { "Failed to create MCP session" } + transport.ready.completeExceptionally(e) } } - serverThread.start() - Thread.sleep(500) + transport.ready.await() transport.handleRequest(call, jsonElement) } else { @@ -173,9 +176,7 @@ class KotlinServerForTsClient { logger.info { "Terminating session: $sessionId" } val transport = serverTransports[sessionId]!! serverTransports.remove(sessionId) - runBlocking { - transport.close() - } + transport.close() call.respond(HttpStatusCode.OK) } else { logger.warn { "Invalid session termination request: $sessionId" } @@ -313,9 +314,10 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { private val logger = KotlinLogging.logger {} private val pendingResponses = ConcurrentHashMap>() private val messageQueue = Channel(Channel.UNLIMITED) + internal val ready = CompletableDeferred() suspend fun stream(call: ApplicationCall) { - logger.debug { "Starting SSE stream for session: $sessionId" } + logger.debug { "Starting Event stream for session: $sessionId" } call.response.header("Cache-Control", "no-cache") call.response.header("Connection", "keep-alive") call.respondTextWriter(ContentType.Text.EventStream) { @@ -331,29 +333,28 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { flush() } } catch (e: Exception) { - logger.warn(e) { "SSE stream terminated for session: $sessionId" } + logger.warn(e) { "Event stream terminated for session: $sessionId" } } finally { - logger.debug { "SSE stream closed for session: $sessionId" } + logger.debug { "Event stream closed for session: $sessionId" } } } } suspend fun handleRequest(call: ApplicationCall, requestBody: JsonElement) { try { - logger.info { "Handling request body: $requestBody" } + logger.trace { "Handling request body: $requestBody" } val message = McpJson.decodeFromJsonElement(requestBody) - logger.info { "Decoded message: $message" } + logger.debug { "Decoded message: $message" } if (message is JSONRPCRequest) { val id = message.id.toString() - logger.info { "Received request with ID: $id, method: ${message.method}" } + logger.debug { "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" } + logger.trace { "Invoking onMessage handler for ID: $id" } _onMessage.invoke(message) - logger.info { "onMessage handler completed" } + logger.trace { "onMessage handler completed for ID: $id" } try { val response = withTimeoutOrNull(10000) { @@ -426,29 +427,28 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { override suspend fun start() { logger.debug { "Starting HTTP server transport for session: $sessionId" } + ready.complete(Unit) } override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { - logger.info { "Sending message: $message" } + logger.trace { "Sending message: $message" } if (message is JSONRPCResponse) { val id = message.id.toString() - logger.info { "Sending response for request ID: $id" } + logger.debug { "Completing 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}" } + logger.debug { "Sending request with ID: ${message.id}" } } else if (message is JSONRPCNotification) { - logger.info { "Sending notification: ${message.method}" } + logger.debug { "Sending notification: ${message.method}" } } - logger.info { "Queueing message for next client request" } messageQueue.send(message) } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt similarity index 85% rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt index dd43b7098..eff91e595 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsClientKotlinServerTestHttp.kt @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractTsClientKotlinServerTest import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind @@ -7,9 +7,9 @@ import io.modelcontextprotocol.kotlin.test.utils.killProcessOnPort import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach -class TsClientKotlinServerTestSse : AbstractTsClientKotlinServerTest() { +class TsClientKotlinServerTestHttp : AbstractTsClientKotlinServerTest() { - override val transportKind = TransportKind.SSE + override val transportKind = TransportKind.HTTP private var port: Int = 0 private lateinit var serverUrl: String @@ -36,11 +36,11 @@ class TsClientKotlinServerTestSse : AbstractTsClientKotlinServerTest() { } override fun runClient(vararg args: String): String = tsClient.use { client -> - val process = client.startSse(listOf(serverUrl) + args.toList(), log = true) + val process = client.startHttp(listOf(serverUrl) + args.toList(), log = true) val output = StringBuilder() process.inputStream.bufferedReader().useLines { lines -> lines.forEach { line -> - println("[TS-CLIENT-SSE] $line") + println("[TS-CLIENT-HTTP] $line") output.append(line).append("\n") } } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt similarity index 90% rename from integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt rename to integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt index ee81ce2c5..9326cc6ee 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/http/TsEdgeCasesTestHttp.kt @@ -1,4 +1,4 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse +package io.modelcontextprotocol.kotlin.sdk.integration.typescript.http import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase @@ -21,9 +21,9 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue import kotlin.test.fail -class TsEdgeCasesTestSse : TsTestBase() { +class TsEdgeCasesTestHttp : TsTestBase() { - override val transportKind = TransportKind.SSE + override val transportKind = TransportKind.HTTP private var port: Int = 0 private lateinit var serverUrl: String @@ -55,7 +55,7 @@ class TsEdgeCasesTestSse : TsTestBase() { @Test @Timeout(30, unit = TimeUnit.SECONDS) fun testInvalidURL() = runTest { - val nonExistentToolCommand = "npx tsx client/sse-client.ts $serverUrl non-existent-tool" + val nonExistentToolCommand = "npx tsx client/http-client.ts $serverUrl non-existent-tool" val nonExistentToolOutput = executeCommandAllowingFailure(nonExistentToolCommand, typescriptDir) assertTrue( @@ -63,7 +63,7 @@ class TsEdgeCasesTestSse : TsTestBase() { "Client should handle non-existent tool gracefully", ) - val invalidUrlCommand = "npx tsx client/sse-client.ts http://localhost:${port + 1000}/mcp greet TestUser" + val invalidUrlCommand = "npx tsx client/http-client.ts http://localhost:${port + 1000}/mcp greet TestUser" val invalidUrlOutput = executeCommandAllowingFailure(invalidUrlCommand, typescriptDir) assertTrue( @@ -84,7 +84,7 @@ class TsEdgeCasesTestSse : TsTestBase() { tempFile.deleteOnExit() val specialCharsContent = tempFile.readText() - val specialCharsCommand = "npx tsx client/sse-client.ts $serverUrl greet \"$specialCharsContent\"" + val specialCharsCommand = "npx tsx client/http-client.ts $serverUrl greet \"$specialCharsContent\"" val specialCharsOutput = executeCommand(specialCharsCommand, typescriptDir) assertTrue( @@ -109,7 +109,7 @@ class TsEdgeCasesTestSse : TsTestBase() { tempFile.deleteOnExit() val largeNameContent = tempFile.readText() - val largePayloadCommand = "npx tsx client/sse-client.ts $serverUrl greet \"$largeNameContent\"" + val largePayloadCommand = "npx tsx client/http-client.ts $serverUrl greet \"$largeNameContent\"" val largePayloadOutput = executeCommand(largePayloadCommand, typescriptDir) tempFile.delete() @@ -157,11 +157,11 @@ class TsEdgeCasesTestSse : TsTestBase() { } val commands = listOf( - "npx tsx client/sse-client.ts $serverUrl greet \"Client1\"", - "npx tsx client/sse-client.ts $serverUrl multi-greet \"Client2\"", - "npx tsx client/sse-client.ts $serverUrl greet \"Client3\"", - "npx tsx client/sse-client.ts $serverUrl", - "npx tsx client/sse-client.ts $serverUrl multi-greet \"Client5\"", + "npx tsx client/http-client.ts $serverUrl greet \"Client1\"", + "npx tsx client/http-client.ts $serverUrl multi-greet \"Client2\"", + "npx tsx client/http-client.ts $serverUrl greet \"Client3\"", + "npx tsx client/http-client.ts $serverUrl", + "npx tsx client/http-client.ts $serverUrl multi-greet \"Client5\"", ) coroutineScope { @@ -242,7 +242,7 @@ class TsEdgeCasesTestSse : TsTestBase() { @Timeout(120, unit = TimeUnit.SECONDS) fun testRapidSequentialRequests() = runBlocking { val outputs = (1..10).map { i -> - val command = "npx tsx client/sse-client.ts $serverUrl greet \"RapidClient$i\"" + val command = "npx tsx client/http-client.ts $serverUrl greet \"RapidClient$i\"" val output = executeCommand(command, typescriptDir) assertTrue( diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt index 2bdd36189..b8c8e9324 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt @@ -1,173 +1,12 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerEdgeCasesTest import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase -import io.modelcontextprotocol.kotlin.sdk.types.TextContent -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -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 -class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() { +class KotlinClientTsServerEdgeCasesTestStdio : AbstractKotlinClientTsServerEdgeCasesTest() { override val transportKind = TransportKind.STDIO - @Test - @Timeout(30, unit = TimeUnit.SECONDS) - fun testNonExistentToolOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> - val nonExistentToolName = "non-existent-tool" - val arguments = mapOf("name" to "TestUser") - - val result = client.callTool(nonExistentToolName, arguments) - assertNotNull(result, "Tool call result should not be null") - - assertTrue(result.isError ?: false, "isError should be true for non-existent tool") - - val textContent = result.content.firstOrNull { it is TextContent } as? TextContent - assertNotNull(textContent, "Error content should be present in the result") - - val errorText = textContent.text - assertTrue( - errorText.contains("non-existent-tool") && errorText.contains("not found"), - "Error message should indicate the tool was not found", - ) - } - } - - @Test - @Timeout(30, unit = TimeUnit.SECONDS) - fun testSpecialCharactersInArgumentsOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> - val specialChars = "!@#$%^&*()_+{}[]|\\:;\"'<>.,?/" - val arguments = mapOf("name" to specialChars) - - val result = client.callTool("greet", arguments) - assertNotNull(result, "Tool call result should not be null") - - val textContent = result.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 testLargePayloadOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> - 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 - 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(30, unit = TimeUnit.SECONDS) - fun testConcurrentRequestsOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> - val concurrentCount = 5 - val responses = coroutineScope { - val results = (1..concurrentCount).map { i -> - 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 textContent = result.content.firstOrNull { it is TextContent } as? TextContent - assertNotNull(textContent, "Text content should be present for client $i") - - textContent.text - } - } - 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 testInvalidArgumentsOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> - val invalidArguments = mapOf( - "name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))), - ) - - val result = client.callTool("greet", invalidArguments) - assertNotNull(result, "Tool call result should not be null") - - val callResult = result - assertTrue(callResult.isError ?: false, "isError should be true for invalid arguments") - - val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent - assertNotNull(textContent, "Error content should be present in the result") - - val errorText = textContent.text - assertTrue( - errorText.contains("Invalid arguments") && errorText.contains("greet"), - "Error message should indicate invalid arguments for tool greet", - ) - } - } - - @Test - @Timeout(30, unit = TimeUnit.SECONDS) - fun testMultipleToolCallsOverStdio(): Unit = runBlocking(Dispatchers.IO) { - withClientStdio { client: Client, _ -> - 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 textContent = result.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", - ) - } - } - } + override suspend fun useClient(block: suspend (Client) -> T): T = withClientStdio { c, _ -> block(c) } } diff --git a/integration-test/src/jvmTest/typescript/client/sse-client.ts b/integration-test/src/jvmTest/typescript/client/http-client.ts similarity index 67% rename from integration-test/src/jvmTest/typescript/client/sse-client.ts rename to integration-test/src/jvmTest/typescript/client/http-client.ts index 0acc256f5..5956a37ee 100644 --- a/integration-test/src/jvmTest/typescript/client/sse-client.ts +++ b/integration-test/src/jvmTest/typescript/client/http-client.ts @@ -2,14 +2,14 @@ import {Client} from '@modelcontextprotocol/sdk/client/index'; import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp'; const args = process.argv.slice(2); -const serverUrl = args[0] || 'http://localhost:3001/mcp'; +const serverUrl = args[0] || 'http://localhost:3000/mcp'; const toolName = args[1]; const toolArgs = args.slice(2); -const PROTOCOL_VERSION = "2024-11-05"; +const PROTOCOL_VERSION = "2025-11-25"; async function main() { if (!toolName) { - console.log('Usage: npx tsx client/sse-client.ts [server-url] [tool-args...]'); + console.log('Usage: npx tsx client/http-client.ts [server-url] [tool-args...]'); console.log('Using default server URL:', serverUrl); console.log('Available utils will be listed after connection'); } @@ -22,6 +22,10 @@ async function main() { const client = new Client({ name: 'test-client', version: '1.0.0' + }, { + fallbackNotificationHandler: async (notification) => { + console.log('Notification:', notification.method, JSON.stringify(notification)); + } }); const transport = new StreamableHTTPClientTransport(new URL(serverUrl)); @@ -30,37 +34,20 @@ async function main() { await client.connect(transport, {protocolVersion: PROTOCOL_VERSION}); console.log('Connected to server'); - try { - if (typeof (client as any).on === 'function') { - (client as any).on('notification', (n: any) => { - try { - const method = (n && (n.method || (n.params && n.params.method))) || 'unknown'; - console.log('Notification:', method, JSON.stringify(n)); - } catch { - console.log('Notification: '); - } - }); - } - } catch { - } - const toolsResult = await client.listTools(); const tools = toolsResult.tools; - console.log('Available utils:', tools.map((t: { name: any; }) => t.name).join(', ')); + console.log('Available utils:', tools.map((t) => t.name).join(', ')); if (!toolName) { - await client.close(); return; } - const tool = tools.find((t: { name: string; }) => t.name === toolName); + const tool = tools.find((t) => t.name === toolName); if (!tool) { - console.error(`Tool "${toolName}" not found`); - // @ts-ignore - process.exit(1); + throw new Error(`Tool "${toolName}" not found`); } - const toolArguments = {}; + const toolArguments: Record = {}; if (toolName === "greet" && toolArgs.length > 0) { toolArguments["name"] = toolArgs[0]; @@ -93,8 +80,7 @@ async function main() { } catch (error) { console.error('Error:', error); - // @ts-ignore - process.exit(1); + process.exitCode = 1; } finally { await client.close(); console.log('Disconnected from server'); @@ -103,6 +89,5 @@ async function main() { main().catch(error => { console.error('Unhandled error:', error); - // @ts-ignore process.exit(1); }); diff --git a/integration-test/src/jvmTest/typescript/client/stdio-client.ts b/integration-test/src/jvmTest/typescript/client/stdio-client.ts index 82c25deb9..a9c765c39 100644 --- a/integration-test/src/jvmTest/typescript/client/stdio-client.ts +++ b/integration-test/src/jvmTest/typescript/client/stdio-client.ts @@ -3,7 +3,7 @@ import {Client} from '@modelcontextprotocol/sdk/client/index'; const args = process.argv.slice(2); const toolName = args[0]; const toolArgs = args.slice(1); -const PROTOCOL_VERSION = "2024-11-05"; +const PROTOCOL_VERSION = "2025-11-25"; async function main() { if (!toolName) { @@ -65,6 +65,10 @@ async function main() { const client = new Client({ name: 'test-client', version: '1.0.0' + }, { + fallbackNotificationHandler: async (notification) => { + console.error('Notification:', notification.method, JSON.stringify(notification)); + } }); const transport = new MinimalStdioClientTransport(); @@ -73,21 +77,6 @@ async function main() { await client.connect(transport, {protocolVersion: PROTOCOL_VERSION}); console.error('Connected to server over stdio'); - try { - if (typeof (client as any).on === 'function') { - (client as any).on('notification', (n: any) => { - try { - const method = (n && (n.method || (n.params && n.params.method))) || 'unknown'; - console.error('Notification:', method, JSON.stringify(n)); - } catch { - console.error('Notification: '); - } - }); - } - } catch { - // ignore - } - const toolsResult = await client.listTools(); const tools = toolsResult.tools; console.error('Available utils:', tools.map((t: { name: any; }) => t.name).join(', ')); diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index fddbba438..1f26245f7 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -8,18 +8,14 @@ "name": "mcp-typescript-test-sdk", "version": "1.0.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.26.0", - "body-parser": "^2.2.2", + "@modelcontextprotocol/sdk": "^1.28.0", "express": "^5.2.1", - "uuid": "^13.0.0", "zod": "^4.3.5" }, "devDependencies": { - "@eslint/js": "^9.39.2", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^24.10.6", - "@types/uuid": "^10.0.0", "cors": "^2.8.5", "tsx": "^4.21.0", "typescript": "^5.9.3" @@ -467,23 +463,10 @@ "node": ">=18" } }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -493,9 +476,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz", + "integrity": "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -640,13 +623,6 @@ "@types/node": "*" } }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "dev": true, - "license": "MIT" - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1029,12 +1005,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", "license": "MIT", "dependencies": { - "ip-address": "10.0.1" + "ip-address": "10.1.0" }, "engines": { "node": ">= 16" @@ -1217,9 +1193,9 @@ } }, "node_modules/hono": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", - "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", + "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -1268,9 +1244,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", "engines": { "node": ">= 12" @@ -1786,19 +1762,6 @@ "node": ">= 0.8" } }, - "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index 6972151ab..3ce560cb0 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -4,23 +4,19 @@ "private": true, "scripts": { "client:stdio": "tsx client/stdio-client.ts", - "client:sse": "tsx client/sse-client.ts http://localhost:3000/mcp greet Hello", + "client:http": "tsx client/http-client.ts http://localhost:3000/mcp greet Hello", "server:stdio": "tsx server/stdio-server.ts", - "server:sse": "tsx server/sse-server.ts" + "server:http": "tsx server/http-server.ts" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.26.0", - "body-parser": "^2.2.2", + "@modelcontextprotocol/sdk": "^1.28.0", "express": "^5.2.1", - "uuid": "^13.0.0", "zod": "^4.3.5" }, "devDependencies": { - "@eslint/js": "^9.39.2", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^24.10.6", - "@types/uuid": "^10.0.0", "cors": "^2.8.5", "tsx": "^4.21.0", "typescript": "^5.9.3" diff --git a/integration-test/src/jvmTest/typescript/server/sse-server.ts b/integration-test/src/jvmTest/typescript/server/http-server.ts similarity index 80% rename from integration-test/src/jvmTest/typescript/server/sse-server.ts rename to integration-test/src/jvmTest/typescript/server/http-server.ts index 48ea45389..c42e115f9 100644 --- a/integration-test/src/jvmTest/typescript/server/sse-server.ts +++ b/integration-test/src/jvmTest/typescript/server/http-server.ts @@ -1,30 +1,24 @@ -// @ts-ignore import express, {Request, Response} from 'express'; import {randomUUID} from 'node:crypto'; -// @ts-ignore import cors from 'cors'; import {McpServer} from '@modelcontextprotocol/sdk/server/mcp'; -import {StreamableHTTPServerTransport,} from '@modelcontextprotocol/sdk/server/streamableHttp'; +import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp'; import {registerTestUtils} from './server-common'; import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '@modelcontextprotocol/sdk/server/auth/router'; import {requireBearerAuth} from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth'; -import {isInitializeRequest,} from '@modelcontextprotocol/sdk/types'; +import {isInitializeRequest} from '@modelcontextprotocol/sdk/types'; import {InMemoryEventStore} from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore'; import {setupAuthServer} from '@modelcontextprotocol/sdk/examples/server/demoInMemoryOAuthProvider'; import {checkResourceAllowed} from '@modelcontextprotocol/sdk/shared/auth-utils'; import {OAuthMetadata} from "@modelcontextprotocol/sdk/shared/auth"; -// import {type OAuthMetadata} from '@modelcontextprotocol/sdk/auth.js'; - async function main() { - // Check for OAuth flag const useOAuth = process.argv.includes('--oauth'); const strictOAuth = process.argv.includes('--oauth-strict'); -// Create an MCP server with implementation details const getServer = () => { const server = new McpServer({ name: 'simple-streamable-http-server', @@ -42,16 +36,15 @@ async function main() { const app = express(); app.use(express.json()); -// Allow CORS all domains, expose the Mcp-Session-Id header + // Allow CORS all domains, expose the Mcp-Session-Id header app.use(cors({ - origin: '*', // Allow all origins + origin: '*', exposedHeaders: ["Mcp-Session-Id"] })); -// Set up OAuth if enabled + // Set up OAuth if enabled let authMiddleware = null; if (useOAuth) { - // Create auth middleware for MCP endpoints const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`); const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`); @@ -79,7 +72,6 @@ async function main() { }).toString() }); - if (!response.ok) { throw new Error(`Invalid or expired token: ${await response.text()}`); } @@ -95,7 +87,6 @@ async function main() { } } - // Convert the response to AuthInfo format return { token, clientId: data.client_id, @@ -104,7 +95,7 @@ async function main() { }; } } - // Add metadata routes to the main MCP server + app.use(mcpAuthMetadataRouter({ oauthMetadata, resourceServerUrl: mcpServerUrl, @@ -119,10 +110,10 @@ async function main() { }); } -// Map to store transports by session ID + // Map to store transports by session ID const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; -// MCP POST endpoint with optional auth + // MCP POST endpoint with optional auth const mcpPostHandler = async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; if (sessionId) { @@ -137,23 +128,18 @@ async function main() { try { let transport: StreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { - // Reuse existing transport transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { - // New initialization request const eventStore = new InMemoryEventStore(); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), - eventStore, // Enable resumability + eventStore, onsessioninitialized: (sessionId) => { - // Store the transport by session ID when session is initialized - // This avoids race conditions where requests might come in before the session is stored console.log(`Session initialized with ID: ${sessionId}`); transports[sessionId] = transport; } }); - // Set up onclose handler to clean up transport when closed transport.onclose = () => { const sid = transport.sessionId; if (sid && transports[sid]) { @@ -162,15 +148,12 @@ async function main() { } }; - // Connect the transport to the MCP server BEFORE handling the request - // so responses can flow back through the same transport const server = getServer(); await server.connect(transport); await transport.handleRequest(req, res, req.body); - return; // Already handled + return; } else { - // Invalid request - no session ID or not initialization request res.status(400).json({ jsonrpc: '2.0', error: { @@ -182,8 +165,6 @@ async function main() { return; } - // Handle the request with existing transport - no need to reconnect - // The existing transport is already connected to the server await transport.handleRequest(req, res, req.body); } catch (error) { console.error('Error handling MCP request:', error); @@ -200,14 +181,13 @@ async function main() { } }; -// Set up routes with conditional auth middleware if (useOAuth && authMiddleware) { app.post('/mcp', authMiddleware, mcpPostHandler); } else { app.post('/mcp', mcpPostHandler); } -// Handle GET requests for SSE streams (using built-in support from StreamableHTTP) + // Handle GET requests for event streams const mcpGetHandler = async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; if (!sessionId || !transports[sessionId]) { @@ -216,29 +196,27 @@ async function main() { } if (useOAuth && req.auth) { - console.log('Authenticated SSE connection from user:', req.auth); + console.log('Authenticated event stream connection from user:', req.auth); } - // Check for Last-Event-ID header for resumability const lastEventId = req.headers['last-event-id'] as string | undefined; if (lastEventId) { console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); } else { - console.log(`Establishing new SSE stream for session ${sessionId}`); + console.log(`Establishing new event stream for session ${sessionId}`); } const transport = transports[sessionId]; await transport.handleRequest(req, res); }; -// Set up GET route with conditional auth middleware if (useOAuth && authMiddleware) { app.get('/mcp', authMiddleware, mcpGetHandler); } else { app.get('/mcp', mcpGetHandler); } -// Handle DELETE requests for session termination (according to MCP spec) + // Handle DELETE requests for session termination (according to MCP spec) const mcpDeleteHandler = async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; if (!sessionId || !transports[sessionId]) { @@ -259,7 +237,6 @@ async function main() { } }; -// Set up DELETE route with conditional auth middleware if (useOAuth && authMiddleware) { app.delete('/mcp', authMiddleware, mcpDeleteHandler); } else { @@ -274,11 +251,10 @@ async function main() { console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`); }); -// Handle server shutdown + // Handle server shutdown process.on('SIGINT', async () => { console.log('Shutting down server...'); - // Close all active transports to properly clean up resources for (const sessionId in transports) { try { console.log(`Closing transport for session ${sessionId}`); @@ -291,7 +267,6 @@ async function main() { console.log('Server shutdown complete'); process.exit(0); }); - } main().catch((err) => { diff --git a/integration-test/src/jvmTest/typescript/tsconfig.json b/integration-test/src/jvmTest/typescript/tsconfig.json new file mode 100644 index 000000000..58e3c6780 --- /dev/null +++ b/integration-test/src/jvmTest/typescript/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["**/*.ts"] +} diff --git a/kotlin-sdk-client/detekt-baseline-test.xml b/kotlin-sdk-client/detekt-baseline-test.xml index b16e8eb1c..dd6adfab3 100644 --- a/kotlin-sdk-client/detekt-baseline-test.xml +++ b/kotlin-sdk-client/detekt-baseline-test.xml @@ -4,8 +4,6 @@ AbstractClassCanBeConcreteClass:AbstractStreamableHttpClientTest.kt:AbstractStreamableHttpClientTest$AbstractStreamableHttpClientTest ForbiddenComment:StreamableHttpClientTest.kt:StreamableHttpClientTest$// TODO: how to get notifications via Client API? - InjectDispatcher:StdioClientTransportErrorHandlingTest.kt:StdioClientTransportErrorHandlingTest$IO - InjectDispatcher:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$Default LongParameterList:MockMcp.kt:MockMcp$fun handleJSONRPCRequest LongParameterList:MockMcp.kt:MockMcp$fun handleWithResult LongParameterList:MockMcp.kt:MockMcp$fun onInitialize diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt index ee7316254..32f55c9ca 100644 --- a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/testing.kt @@ -8,7 +8,6 @@ import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -@Suppress("InjectDispatcher") // this is constant private val defaultTestDispatcher: CoroutineContext = Dispatchers.IO.limitedParallelism(100) private val defaultTestTimeout: Duration = 5.seconds From cd91eed35de68efe2e426ed0758a8f4097c39ff2 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Thu, 26 Mar 2026 13:45:58 +0100 Subject: [PATCH 035/187] chore: simplify and clean up CI workflows (#617) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove redundant Gradle flags, env vars, and unused settings across all workflows - Minimize GitHub Actions permissions to least-privilege - Drop Windows build matrix (JVM tests covered by Linux) - Simplify CodeQL workflow (remove matrix, unused permissions, stale triggers) - Clean up gradle.properties (remove unused KMP settings) ## How Has This Been Tested? ## Breaking Changes — ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/apidocs.yaml | 5 +--- .github/workflows/build.yml | 44 +++++++++++++--------------- .github/workflows/codeql.yml | 38 +++++------------------- .github/workflows/gradle-publish.yml | 27 ++--------------- .github/workflows/samples.yml | 7 ++--- gradle.properties | 6 ---- 6 files changed, 34 insertions(+), 93 deletions(-) diff --git a/.github/workflows/apidocs.yaml b/.github/workflows/apidocs.yaml index 41e15f3f0..5c7ed4cd8 100644 --- a/.github/workflows/apidocs.yaml +++ b/.github/workflows/apidocs.yaml @@ -25,9 +25,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - submodules: true # Fetch Hugo themes (true OR recursive) - fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v5 @@ -41,7 +38,7 @@ jobs: - name: Generate Dokka Site run: |- - ./gradlew clean knit docs:classes docs:knitCheck dokkaGenerate + ./gradlew knit docs:classes docs:knitCheck dokkaGenerate - name: Upload artifact uses: actions/upload-pages-artifact@v4 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f97721bc6..4fd25226c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,14 +13,12 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: - checks: write - pull-requests: write # only required if `comment: true` was enabled + contents: read env: JAVA_VERSION: 21 JAVA_DISTRIBUTION: temurin - JAVA_OPTS: "-Dfile.encoding=UTF-8 -Djava.awt.headless=true" - GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy=in-process" + GRADLE_OPTS: "-Dorg.gradle.daemon=false" jobs: build: @@ -36,17 +34,12 @@ jobs: include: - os: ubuntu-latest job-name: "Linux Tests" - gradle-tasks: "build ktlintCheck koverLog koverHtmlReport koverXmlReport publishToMavenLocal -Pversion=1-SNAPSHOT" - max-workers: 3 - - - os: windows-latest - job-name: "Windows Tests" - gradle-tasks: "jvmTest -x :integration-test:jvmTest" + gradle-tasks: "build" max-workers: 3 - os: macos-latest job-name: "iOS Simulator tests" - gradle-tasks: "linkIosSimulatorArm64 iosSimulatorArm64Test" + gradle-tasks: "iosSimulatorArm64Test" max-workers: 2 steps: @@ -75,19 +68,25 @@ jobs: validate-wrappers: true add-job-summary: 'always' cache-read-only: ${{ github.ref != 'refs/heads/main' }} - cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} gradle-home-cache-includes: | caches notifications - sdks ../.konan/** - name: ${{ matrix.job-name }} run: |- - ./gradlew ${{ matrix.gradle-tasks }} -x :conformance-test:test --max-workers ${{ matrix.max-workers }} --continue --rerun-tasks --configure-on-demand --parallel + ./gradlew ${{ matrix.gradle-tasks }} --max-workers ${{ matrix.max-workers }} --continue --rerun-tasks + + - name: Coverage Report + if: ${{ success() && matrix.os == 'ubuntu-latest' }} + run: ./gradlew koverLog koverHtmlReport koverXmlReport + + - name: Publish to Maven Local + if: ${{ success() && matrix.os == 'ubuntu-latest' }} + run: ./gradlew publishToMavenLocal -Pversion=1-SNAPSHOT - name: Upload Reports - if: always() + if: failure() uses: actions/upload-artifact@v7 with: name: reports-build-${{ matrix.os }} @@ -100,10 +99,8 @@ jobs: with: annotate_only: true detailed_summary: true - flaky_summary: true group_suite: true include_empty_in_summary: false - include_time_in_summary: true report_paths: '**/test-results/**/TEST-*.xml' truncate_stack_traces: false @@ -127,7 +124,7 @@ jobs: - name: Archive Maven Local Repository if: ${{ success() && matrix.os == 'ubuntu-latest' }} run: | - cd ~/.m2/repository && zip -r $GITHUB_WORKSPACE/local-maven-repository.zip . + cd ~/.m2/repository/io/modelcontextprotocol && zip -r $GITHUB_WORKSPACE/local-maven-repository.zip . - name: Upload Maven Local Repository if: ${{ success() && matrix.os == 'ubuntu-latest' }} @@ -159,7 +156,6 @@ jobs: validate-wrappers: true add-job-summary: 'always' cache-read-only: ${{ github.ref != 'refs/heads/main' }} - cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} gradle-home-cache-includes: | caches notifications @@ -168,12 +164,14 @@ jobs: - name: Run Knit run: |- - ./gradlew knit docs:classes docs:knitCheck -x :conformance-test:test --max-workers 3 --continue --rerun-tasks --configure-on-demand --parallel + ./gradlew knit docs:classes docs:knitCheck --max-workers 3 --continue --rerun-tasks samples: needs: [ build ] runs-on: ubuntu-latest + permissions: + contents: read strategy: fail-fast: false matrix: @@ -185,8 +183,6 @@ jobs: name: "Build Sample: ${{ matrix.sample }}" timeout-minutes: 10 - env: - JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" steps: - uses: actions/checkout@v6 @@ -215,8 +211,8 @@ jobs: - name: Extract Maven Local Repository run: | - mkdir -p ~/.m2/repository - unzip -o local-maven-repository.zip -d ~/.m2/repository + mkdir -p ~/.m2/repository/io/modelcontextprotocol + unzip -o local-maven-repository.zip -d ~/.m2/repository/io/modelcontextprotocol - name: "Build Sample: ${{ matrix.sample }}" working-directory: ./samples/${{ matrix.sample }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4657da917..41d1fdea9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,16 +1,14 @@ name: "CodeQL Advanced" on: - push: - branches: [ "main" ] pull_request: branches: [ "main" ] schedule: - - cron: '0 4 * * 0' + - cron: '0 7 * * 1' # Monday 8:00 CET (7:00 UTC) concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + cancel-in-progress: true env: JAVA_VERSION: 21 @@ -18,30 +16,16 @@ env: jobs: analyze: - name: Analyze (${{ matrix.language }}) + name: Analyze runs-on: ubuntu-latest timeout-minutes: 30 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: - fail-fast: false - matrix: - language: [ java-kotlin ] - steps: - name: Checkout repository uses: actions/checkout@v6 - with: - fetch-depth: 0 - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v5 @@ -49,11 +33,10 @@ jobs: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.JAVA_VERSION }} - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: - languages: ${{ matrix.language }} + languages: java-kotlin build-mode: manual - name: Setup Gradle @@ -64,18 +47,11 @@ jobs: - name: Build Kotlin sources run: | - ./gradlew \ - :kotlin-sdk-core:compileKotlinJvm \ - :kotlin-sdk-client:compileKotlinJvm \ - :kotlin-sdk-server:compileKotlinJvm \ - :kotlin-sdk:compileKotlinJvm \ - :integration-test:compileKotlinJvm \ - :conformance-test:testClasses \ - detekt \ + ./gradlew compileKotlinJvm :conformance-test:compileKotlin \ -Pkotlin.incremental=false \ - --no-daemon --stacktrace --rerun-tasks + --no-daemon --rerun-tasks --stacktrace - name: Analyze uses: github/codeql-action/analyze@v4 with: - category: '/language:${{ matrix.language }}' + category: '/language:java-kotlin' diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index 5acfce8c8..ed9a63a23 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -1,10 +1,3 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created -# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle - name: Release on: @@ -17,8 +10,7 @@ jobs: runs-on: macos-latest environment: release env: - JAVA_OPTS: "-Xmx4g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=768m -Djava.awt.headless=true" - GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy=in-process" + GRADLE_OPTS: "-Dorg.gradle.daemon=false" permissions: contents: write @@ -35,21 +27,8 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - - name: Clean Build with Gradle - run: ./gradlew clean build -x :conformance-test:test --rerun-tasks --max-workers 2 --continue --configure-on-demand --parallel - - - name: Publish Test Report - uses: mikepenz/action-junit-report@v6 - if: ${{ !cancelled() }} - with: - annotate_only: true - detailed_summary: true - flaky_summary: true - group_suite: true - include_empty_in_summary: false - include_time_in_summary: true - report_paths: '**/test-results/**/TEST-*.xml' - truncate_stack_traces: false + - name: Build + run: ./gradlew build --rerun-tasks --max-workers 2 --continue - name: Publish to Maven Central Portal id: publish diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 373f00a56..632b71039 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -19,6 +19,7 @@ concurrency: env: JAVA_VERSION: 21 JAVA_DISTRIBUTION: temurin + GRADLE_OPTS: "-Dorg.gradle.daemon=false" jobs: build: @@ -34,8 +35,6 @@ jobs: name: Build Sample (${{ matrix.sample }}) timeout-minutes: 10 - env: - JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" steps: - uses: actions/checkout@v6 @@ -54,10 +53,10 @@ jobs: - name: "Build Sample: ${{ matrix.sample }}" working-directory: ./samples/${{ matrix.sample }} - run: ./gradlew --no-daemon build + run: ./gradlew build - name: Upload Reports - if: ${{ !cancelled() }} + if: failure() uses: actions/upload-artifact@v7 with: name: reports-samples-${{ matrix.sample }} diff --git a/gradle.properties b/gradle.properties index 45b067c88..43b4acb20 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,16 +1,10 @@ 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 -# Build JS targets using npm package manager https://kotlinlang.org/docs/js-project-setup.html#npm-dependencies -kotlin.js.yarn=false # MPP -kotlin.mpp.enableCInteropCommonization=true kotlin.native.ignoreDisabledTargets=true group=io.modelcontextprotocol From c77ebe0c00ac8be633f530b7355141c61ddbc5cd Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:36:01 +0200 Subject: [PATCH 036/187] refactor(core): Fix Kotlin language and api version to 2.1 in build config (#620) ## Fix Kotlin language and api version to 2.1 - Remove language constructs not supported in Kotlin 2.1 ## Motivation and Context To ensure binary compatibility ## How Has This Been Tested? Regression tests ## Breaking Changes ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [x] Build update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context --- .../main/kotlin/mcp.multiplatform.gradle.kts | 3 +++ .../http/StreamableHttpClientTransportTest.kt | 21 +++++++++---------- .../kotlin/sdk/types/request.kt | 6 +++--- .../kotlin/sdk/types/serializers.kt | 6 +++--- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts index 36097ffa0..f8d89c0d9 100644 --- a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts +++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts @@ -5,6 +5,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { kotlin("multiplatform") @@ -15,6 +16,8 @@ plugins { kotlin { compilerOptions { + languageVersion = KotlinVersion.KOTLIN_2_1 + apiVersion = KotlinVersion.KOTLIN_2_1 freeCompilerArgs = listOf( "-Wextra", diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt index 55df3e06e..67b2f07fb 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt @@ -148,18 +148,17 @@ class StreamableHttpClientTransportTest { ) val transport = createTransport { request -> - when ( - val msg = McpJson.decodeFromString( - (request.body as TextContent).text, - ) - ) { - is JSONRPCRequest if msg.method == "initialize" -> respond( + val msg = McpJson.decodeFromString( + (request.body as TextContent).text, + ) + when { + msg is JSONRPCRequest && msg.method == "initialize" -> respond( content = "", status = HttpStatusCode.OK, headers = headersOf("mcp-session-id", "test-session-id"), ) - is JSONRPCNotification if msg.method == "test" -> { + msg is JSONRPCNotification && msg.method == "test" -> { assertEquals("test-session-id", request.headers["mcp-session-id"]) respond( content = "", @@ -246,8 +245,8 @@ class StreamableHttpClientTransportTest { var sseStarted = false val transport = createTransport { request -> - when (request.method) { - HttpMethod.Post if request.body.toString().contains("notifications/initialized") -> { + when { + request.method == HttpMethod.Post && request.body.toString().contains("notifications/initialized") -> { respond( content = "", status = HttpStatusCode.Accepted, @@ -256,7 +255,7 @@ class StreamableHttpClientTransportTest { } // Handle SSE connection - HttpMethod.Get -> { + request.method == HttpMethod.Get -> { sseStarted = true val sseContent = buildString { // Server sends various notifications @@ -288,7 +287,7 @@ class StreamableHttpClientTransportTest { } // Handle regular notifications - HttpMethod.Post -> { + request.method == HttpMethod.Post -> { respond( content = "", status = HttpStatusCode.Accepted, diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt index dc7bd3bef..683186c15 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt @@ -16,9 +16,9 @@ import kotlin.jvm.JvmInline public value class RequestMeta(public val json: JsonObject) { public val progressToken: ProgressToken? get() = json["progressToken"]?.let { element -> - when (element) { - is JsonPrimitive if (element.isString) -> ProgressToken(element.content) - is JsonPrimitive if (element.longOrNull != null) -> ProgressToken(element.long) + when { + element is JsonPrimitive && element.isString -> ProgressToken(element.content) + element is JsonPrimitive && element.longOrNull != null -> ProgressToken(element.long) else -> null } } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt index 2d20d425d..792c63605 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt @@ -414,9 +414,9 @@ internal object JSONRPCMessagePolymorphicSerializer : * Supports both string and number IDs. */ internal object RequestIdPolymorphicSerializer : JsonContentPolymorphicSerializer(RequestId::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy = when (element) { - is JsonPrimitive if (element.isString) -> RequestId.StringId.serializer() - is JsonPrimitive if (element.longOrNull != null) -> RequestId.NumberId.serializer() + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = when { + element is JsonPrimitive && element.isString -> RequestId.StringId.serializer() + element is JsonPrimitive && element.longOrNull != null -> RequestId.NumberId.serializer() else -> throw SerializationException("Invalid RequestId type: $element") } } From bd4cf0c948203380cdb96503e332b8e3b2ae3e2e Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:25:13 +0200 Subject: [PATCH 037/187] docs: add missing KDoc comments for public functions, properties, and classes (#621) --- .../modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt | 3 +++ .../kotlin/sdk/types/capabilities.dsl.kt | 1 + .../io/modelcontextprotocol/kotlin/sdk/types/common.kt | 3 +++ .../modelcontextprotocol/kotlin/sdk/types/content.dsl.kt | 1 + .../io/modelcontextprotocol/kotlin/sdk/types/content.kt | 1 + .../io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt | 2 ++ .../io/modelcontextprotocol/kotlin/sdk/types/sampling.kt | 1 + .../io/modelcontextprotocol/kotlin/sdk/server/Server.kt | 7 +++++++ .../kotlin/sdk/server/StreamableHttpServerTransport.kt | 2 ++ 9 files changed, 21 insertions(+) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt index d3fc61fd4..d4c8b4347 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt @@ -16,6 +16,7 @@ public class ReadBuffer { private val buffer: Buffer = Buffer() + /** Appends raw bytes to the internal buffer for subsequent message parsing. */ public fun append(chunk: ByteArray) { buffer.write(chunk) } @@ -86,6 +87,7 @@ public class ReadBuffer { } } + /** Clears all buffered data, discarding any partially received messages. */ public fun clear() { buffer.clear() } @@ -93,4 +95,5 @@ public class ReadBuffer { internal fun deserializeMessage(line: String): JSONRPCMessage = McpJson.decodeFromString(line) +/** Serializes a [JSONRPCMessage] to its JSON string representation with a trailing newline. */ public fun serializeMessage(message: JSONRPCMessage): String = McpJson.encodeToString(message) + "\n" diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt index b132a5ed8..146eabfb2 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt @@ -171,6 +171,7 @@ public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { */ public fun experimental(block: JsonObjectBuilder.() -> Unit): Unit = experimental(buildJsonObject(block)) + /** Constructs the [ClientCapabilities] instance from the current builder state. */ @PublishedApi internal fun build(): ClientCapabilities = ClientCapabilities( sampling = sampling, diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt index 7ec9c076e..3ae89979d 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt @@ -41,8 +41,10 @@ public sealed interface WithMeta { */ public typealias ProgressToken = RequestId +/** Creates a [ProgressToken] from a string value. */ public fun ProgressToken(value: String): ProgressToken = RequestId(value) +/** Creates a [ProgressToken] from a numeric value. */ public fun ProgressToken(value: Long): ProgressToken = RequestId(value) // ============================================================================ @@ -127,6 +129,7 @@ public sealed interface Reference { public val type: ReferenceType } +/** Discriminator for [Reference] subtypes used in completion and other operations. */ @Serializable public enum class ReferenceType(public val value: String) { @SerialName("ref/prompt") diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.dsl.kt index e538af572..8718dec48 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.dsl.kt @@ -88,6 +88,7 @@ public abstract class MediaContentBuilder { meta = buildJsonObject(block) } + /** Constructs the [MediaContent] instance from the current builder state. */ @PublishedApi internal abstract fun build(): MediaContent } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt index 0e6377f55..45cfd0ddb 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt @@ -8,6 +8,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject +/** Discriminator values for the polymorphic [ContentBlock] hierarchy. */ @Serializable public enum class ContentTypes(public val value: String) { @SerialName("text") diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt index 6a53d350f..f42139f55 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt @@ -40,10 +40,12 @@ public fun RequestId(value: Long): RequestId = RequestId.NumberId(value) @Serializable(with = RequestIdPolymorphicSerializer::class) public sealed interface RequestId { + /** A string-based request identifier. */ @JvmInline @Serializable public value class StringId(public val value: String) : RequestId + /** A numeric request identifier. */ @JvmInline @Serializable public value class NumberId(public val value: Long) : RequestId diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt index 41dc864d4..1ade1bfc5 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt @@ -243,6 +243,7 @@ public data class CreateMessageResult( override val meta: JsonObject? = null, ) : ClientResult +/** The reason why the LLM stopped generating tokens. */ @JvmInline @Serializable public value class StopReason(public val value: String) { diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index afed86a34..e566d099f 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -185,6 +185,7 @@ public open class Server( block(this) } + /** Closes this server, shutting down the notification service and all active sessions. */ public suspend fun close() { logger.debug { "Closing MCP server" } notificationService.close() @@ -718,6 +719,7 @@ public open class Server( // Start the ServerSession / ClientConnection redirection section + /** Returns the [ClientConnection] for the session identified by [sessionId]. */ public fun clientConnection(sessionId: String): ClientConnection = sessionRegistry.getSession(sessionId).clientConnection @@ -827,18 +829,22 @@ public open class Server( // End the ServerSession / ClientConnection redirection section // Start the notification handling section + + /** Registers a notification [handler] for the given [method] on all active sessions. */ public fun setNotificationHandler(method: Method, handler: (notification: T) -> Deferred) { sessions.forEach { (_, session) -> session.setNotificationHandler(method, handler) } } + /** Removes the notification handler for the given [method] from all active sessions. */ public fun removeNotificationHandler(method: Method) { sessions.forEach { (_, session) -> session.removeNotificationHandler(method) } } + /** Registers a notification [handler] for the given [method] on a specific session. */ public fun setNotificationHandler( sessionId: String, method: Method, @@ -847,6 +853,7 @@ public open class Server( sessionRegistry.getSessionOrNull(sessionId)?.setNotificationHandler(method, handler) } + /** Removes the notification handler for the given [method] from a specific session. */ public fun removeNotificationHandler(sessionId: String, method: Method) { sessionRegistry.getSessionOrNull(sessionId)?.removeNotificationHandler(method) } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index d0bbcfbbf..821c525a1 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -418,6 +418,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } + /** Handles an HTTP GET request by establishing a standalone SSE stream for server-initiated notifications. */ @Suppress("ReturnCount") public suspend fun handleGetRequest(session: ServerSSESession?, call: ApplicationCall) { // NOTE: enableJsonResponse only controls how POST responses are delivered (JSON vs. SSE). @@ -465,6 +466,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat awaitCancellation() } + /** Handles an HTTP DELETE request by closing the session and the transport. */ public suspend fun handleDeleteRequest(call: ApplicationCall) { if (!validateSession(call) || !validateProtocolVersion(call)) return sessionId?.let { onSessionClosed?.invoke(it) } From 961b29bbe6e0def47337a2c54597e0a5750caa81 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:00:09 +0200 Subject: [PATCH 038/187] chore: bump project version to 0.10.0 in gradle.properties (#623) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 43b4acb20..1d5620a7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,4 +8,4 @@ kotlin.daemon.jvmargs=-Xmx4G kotlin.native.ignoreDisabledTargets=true group=io.modelcontextprotocol -version=0.9.0 +version=0.10.0 From 01689cc2481823617357af2706ce543a49d2929d Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Thu, 26 Mar 2026 20:35:04 +0100 Subject: [PATCH 039/187] chore: improve dependabot configuration (#619) - Add grouped dependency updates for root Gradle (ktor, kotlinx, kotlin, kotest, testing, infrastructure, gradle-plugins) - Track all sample projects via `directories: /samples/*` with separate PRs for ktor, kotlin, and mcp-kotlin-sdk - Track npm dependencies in integration tests - Group github actions updates into a single PR ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- .github/dependabot.yml | 93 ++++++++++++++++++++- .github/workflows/dependabot-auto-merge.yml | 23 +++++ 2 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/dependabot-auto-merge.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8d02aac21..b951eaad3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,20 +1,109 @@ -# Configuration options: -# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#about-the-dependabotyml-file +# yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json +# Configuration: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 + updates: + # Root Gradle project (includes buildSrc) - package-ecosystem: "gradle" directory: "/" schedule: interval: "weekly" + day: "monday" + commit-message: + prefix: "chore(deps):" labels: - "dependencies" - "kotlin" + open-pull-requests-limit: 10 + groups: + ktor: + patterns: + - "io.ktor:*" + kotlinx: + patterns: + - "org.jetbrains.kotlinx:*" + kotlin: + patterns: + - "org.jetbrains.kotlin:*" + kotest: + patterns: + - "io.kotest:*" + testing: + patterns: + - "io.mockk:*" + - "dev.mokksy:*" + - "org.awaitility:*" + - "org.junit.jupiter:*" + infrastructure: + patterns: + - "io.netty:*" + - "io.github.oshai:*" + - "org.slf4j:*" + gradle-plugins: + patterns: + - "dev.detekt" + - "org.jetbrains.kotlinx.kover" + - "org.jlleitschuh.gradle.ktlint" + - "org.jetbrains.kotlinx.knit" + - "org.jetbrains.kotlinx.binary-compatibility-validator" + - "org.openapi.generator" + - "org.jetbrains.dokka:*" + - "com.vanniktech:*" + + # Samples (independent Gradle builds) + - package-ecosystem: "gradle" + directories: + - "/samples/*" + schedule: + interval: "weekly" + day: "monday" + commit-message: + prefix: "chore(deps):" + labels: + - "dependencies" + - "kotlin" + - "samples" + open-pull-requests-limit: 10 + groups: + other-dependencies: + patterns: + - "*" + exclude-patterns: + - "io.ktor*" + - "org.jetbrains.kotlin*" + - "io.modelcontextprotocol*" + + # npm (integration tests) + - package-ecosystem: "npm" + directory: "/integration-test/src/jvmTest/typescript" + schedule: + interval: "weekly" + day: "monday" + commit-message: + prefix: "chore(deps):" + labels: + - "dependencies" + - "javascript" + open-pull-requests-limit: 10 + groups: + all-dependencies: + patterns: + - "*" + # GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" + day: "monday" + commit-message: + prefix: "chore(deps):" labels: - "dependencies" - "github-actions" + open-pull-requests-limit: 10 + groups: + all-actions: + patterns: + - "*" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 000000000..6073684e7 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,23 @@ +name: Auto-merge Dependabot + +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - uses: dependabot/fetch-metadata@v2 + id: metadata + + - if: >- + steps.metadata.outputs.package-ecosystem == 'github_actions' || + steps.metadata.outputs.package-ecosystem == 'npm_and_yarn' + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 681e7e813d82b09a06543ab96b01efc1d4118bea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:01:17 +0200 Subject: [PATCH 040/187] chore(deps): bump the gradle-plugins group with 4 updates (#633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the gradle-plugins group with 4 updates: [org.jetbrains.dokka:dokka-gradle-plugin](https://github.com/Kotlin/dokka), [org.jetbrains.kotlinx.kover](https://github.com/Kotlin/kotlinx-kover), org.jlleitschuh.gradle.ktlint and org.openapi.generator. Updates `org.jetbrains.dokka:dokka-gradle-plugin` from 2.1.0 to 2.2.0
Release notes

Sourced from org.jetbrains.dokka:dokka-gradle-plugin's releases.

2.2.0

Dokka Gradle Plugin

Starting from Dokka 2.1.0, the new Dokka Gradle Plugin is enabled by default. The documentation on kotlinlang.org has been updated accordingly:

Dokka 2.2.0 introduces multiple improvements and fixes:

Note: most of the following changes affect only the new Dokka Gradle Plugin, enabled by default since Dokka 2.1.0

Analysis improvements

Starting from Dokka 2.1.0, the K2 analysis is enabled by default. K2 analysis is now stable, enabled by default, and fully migrated to the new shared Analysis API. This includes the migration to the new KDoc resolution API within the Analysis API.

Dokka 2.2.0 introduces multiple improvements and fixes:

Note: most of the following changes affect only Dokka's K2 analysis, enabled by default since Dokka 2.1.0

  • Allow actual declarations to automatically inherit their documentation from expect counterparts in multiplatform projects (#2493, #4245, #4351)
  • Link resolution improvements:
    • Support references to declarations with quoted names (#3356)
    • Support resolution of links to extensions with type parameters according to KEEP#385 (#3555)
    • Improve handling of ambiguous KDoc links according to KEEP#389 (#3451, #3179, #3632, #4327, #3604)
      • Note: those changes are currently available only under experimental org.jetbrains.dokka.analysis.enableExperimentalKDocResolution system property
  • K2/K1 compatibility improvements:
    • Fix Multiple pages associated with key (#4300)
    • Fix inconsistent constructor rendering for expect/actual annotation (#4055)
    • Fix missing abstract modifier for abstract interface method with redundant open modifier (#4144)
    • Fix working with intersected and overridden fake functions/properties (#3857)
    • Fix rendering of links in @see block (#3680)
    • Fix redundant ? on properties with a type of typealias to nullable type (#4337)
    • Fix the missing default parameter value for inherited (not overridden) members (#4320)
    • Fix duplicate source links for function overloads and properties (#4049, #4338)
    • Fix resolution of links in the second line of KDoc tags (#4332, KT-75215, KT-79783)
  • Improve DRI handling for varargs and properties (#3558, #4347)
  • Context parameters improvements:
    • Fix KDoc links to context parameters (#4389)

... (truncated)

Commits
  • 656ca46 Update Dokka version references to 2.2.0 (#4485)
  • 534e242 Update com.gradle.publish:plugin-publish-plugin and declare Gradle CC compa...
  • abc048d Update the version for the 2.2.0 release
  • 60062bb Detect and handle intersected source roots in Android multi-variant projects ...
  • 0bd8995 Fix DGP reading all Gradle properties (#4468)
  • 06dbbfd Update the version for the 2.2.0-Beta release
  • a81ace7 Update AGP version in tests to stable 9.0.0 (#4420)
  • fd97482 Fix minor oversight in the implemenation: missed + in enum entries (#4411)
  • 8148aa2 Fix unnecesary logging for unresolved links in module documentation (#4413)
  • bf3b5ee Fix suppressGeneratedFiles was unused (#4348)
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlinx.kover` from 0.9.7 to 0.9.8
Release notes

Sourced from org.jetbrains.kotlinx.kover's releases.

0.9.8

Kover Gradle Plugin

Bugfixes

Internal features

Changelog

Sourced from org.jetbrains.kotlinx.kover's changelog.

0.9.8 / 2026-03-25

Kover Gradle Plugin

Internal features

Bugfixes

Commits
  • e329fea Release 0.9.8
  • d006ccd Filled Implementation fields Vendor/Title/Version in MANIFEST.MF
  • 9c300e1 Fixed Android multiplatform library support in case of the violation of the p...
  • e302173 Update publish plugin and declare configuration cache compatibility
  • b95e2bf Fix aggregated plugin docs
  • See full diff in compare view

Updates `org.jlleitschuh.gradle.ktlint` from 14.0.1 to 14.2.0 Updates `org.openapi.generator` from 7.18.0 to 7.21.0 Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 313c8f6a5..b5a22e338 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,14 +3,14 @@ atomicfu = "0.31.0" binaryCompatibilityValidatorPlugin = "0.18.1" detekt = "2.0.0-alpha.2" -dokka = "2.1.0" +dokka = "2.2.0" kotlin = "2.2.21" -kover = "0.9.7" -ktlint = "14.0.1" +kover = "0.9.8" +ktlint = "14.2.0" knit = "0.5.1" mavenPublish = "0.36.0" netty = "4.2.10.Final" -openapi-generator = "7.18.0" +openapi-generator = "7.21.0" # libraries version awaitility = "4.3.0" From aed2439ecd5c6e864c7afc9b4b07f6e169acb404 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:11:06 +0200 Subject: [PATCH 041/187] chore(deps): bump the infrastructure group with 2 updates (#632) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the infrastructure group with 2 updates: [io.github.oshai:kotlin-logging](https://github.com/oshai/kotlin-logging) and [io.netty:netty-bom](https://github.com/netty/netty). Updates `io.github.oshai:kotlin-logging` from 7.0.14 to 8.0.01
Release notes

Sourced from io.github.oshai:kotlin-logging's releases.

8.0.01

What's Changed

Full Changelog: https://github.com/oshai/kotlin-logging/compare/7.0.14...8.0.01

8.0.00-beta-04

What's Changed

Full Changelog: https://github.com/oshai/kotlin-logging/compare/8.0.00-beta-03...8.0.00-beta-04

8.0.00-beta-03

What's Changed

Full Changelog: https://github.com/oshai/kotlin-logging/compare/8.0.00-beta-02...8.0.00-beta-03

8.0.00-beta-02

What's Changed

Full Changelog: https://github.com/oshai/kotlin-logging/compare/8.0.00-beta-01...8.0.00-beta-02

8.0.00-beta-01

What's Changed

Full Changelog: https://github.com/oshai/kotlin-logging/compare/7.0.14...8.0.00-beta-01

Commits
  • b1eb9fc bump version to 8.0.01
  • ff436aa bump version to 8.0.00-beta-05
  • 36e56c3 Fix Darwin OSLog composition failure in historical logs (#594)
  • 4ff0b7d bump version to 8.0.00-beta-04
  • 283f30c Feature: Add JPMS Support (Issue #365) (#592)
  • e5366c2 bump version to 8.0.00-beta-03
  • eb267ab feat: add configuration to suppress startup message
  • 6a67a6f Fix Darwin historical logs redaction using public format string
  • aa51a0d feat: add timestamp to KLoggingEvent
  • 733deaa fix action
  • Additional commits viewable in compare view

Updates `io.netty:netty-bom` from 4.2.10.Final to 4.2.12.Final
Release notes

Sourced from io.netty:netty-bom's releases.

netty-4.2.12.Final

What's Changed

Full Changelog: https://github.com/netty/netty/compare/netty-4.2.11.Final...netty-4.2.12.Final

netty-4.2.11.Final

Security

What's Changed

... (truncated)

Commits
  • 67ce541 [maven-release-plugin] prepare release netty-4.2.12.Final
  • 7074624 Revert "Eliminate redundant bounds checks in CompositeByteBuf accessors" (#16...
  • c3b0a43 [maven-release-plugin] prepare for next development iteration
  • c94a818 [maven-release-plugin] prepare release netty-4.2.11.Final
  • 3b76df1 Merge commit from fork
  • aae944a Auto-port 4.2: Limit the number of Continuation frames per HTTP2 Headers (#16...
  • 6001499 Eliminate redundant bounds checks in CompositeByteBuf accessors (#16525)
  • a7fbb6f JdkZlibDecoder: accumulate decompressed output before firing channelRead (#16...
  • 7937553 Enforce io.netty.maxDirectMemory accounting on all Java versions (#16489)
  • 893ea2e Allocate less in QueryStringDecoder.addParam for typical use case (#16527)
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5a22e338..a3cb75984 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ kover = "0.9.8" ktlint = "14.2.0" knit = "0.5.1" mavenPublish = "0.36.0" -netty = "4.2.10.Final" +netty = "4.2.12.Final" openapi-generator = "7.21.0" # libraries version @@ -19,7 +19,7 @@ coroutines = "1.10.2" kotest = "6.1.3" kotlinx-io = "0.8.2" ktor = "3.2.3" -logging = "7.0.14" +logging = "8.0.01" mockk = "1.14.9" mokksy = "0.8.0" serialization = "1.9.0" From 1cda704694fa920fe986aa634dfbcf7ec25e90c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:28:37 +0000 Subject: [PATCH 042/187] chore(deps): bump the all-actions group with 3 updates (#631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-actions group with 3 updates: [gradle/actions](https://github.com/gradle/actions), [actions/deploy-pages](https://github.com/actions/deploy-pages) and [codecov/codecov-action](https://github.com/codecov/codecov-action). Updates `gradle/actions` from 5 to 6
Release notes

Sourced from gradle/actions's releases.

v6.0.0

[!IMPORTANT] The release of gradle/actions@v6 contains important changes to the license terms. More details in this blog post. TL;DR: By upgrading to v6, you accept the Terms of Use for the gradle-actions-caching component.

Summary

  • Caching functionality of 'gradle-actions' has been extracted into a separate gradle-actions-caching library, and is no longer open-source. See this blog post for more context.
  • Existing, rudimentary, configuration-cache support has been removed, pending a fully functional implementation in gradle-actions-caching.
  • Dependencies updated to address security vulnerabilities

[!IMPORTANT]

Licensing notice

The caching functionality in `gradle-actions` has been extracted into `gradle-actions-caching`, a proprietary commercial component that is not covered by the MIT License. The bundled `gradle-actions-caching` component is licensed and governed by a separate license, available at https://gradle.com/legal/terms-of-use/.

The `gradle-actions-caching` component is used only when caching is enabled and is not loaded or used when caching is disabled.

Use of the `gradle-actions-caching` component is subject to a separate license, available at https://gradle.com/legal/terms-of-use/. If you do not agree to these license terms, do not use the `gradle-actions-caching` component.

What's Changed

Full Changelog: https://github.com/gradle/actions/compare/v5.0.2...v6.0.0

v5.0.2

Summary

This release contains no functional changes. It updates dependencies and known Gradle wrapper checksums.

What's Changed

... (truncated)

Commits
  • 39e147c [bot] Update dist directory
  • 14ac3d6 Bump com.fasterxml.jackson.dataformat:jackson-dataformat-smile from 2.21.1 to...
  • 81fec7a Mention explicit license acceptance in notice (#912)
  • 4ac5b01 [bot] Update dist directory
  • f64284c Mute license warning when terms are accepted (#911)
  • c2457a7 Update tagging instructions for release
  • 8205114 Update Gradle version compatibility information
  • 6710000 Add typing information for use by typesafegithub (#910)
  • 3d0e2a8 Pin version for github actions
  • f663ed9 Ignore internal action files for type validation
  • Additional commits viewable in compare view

Updates `actions/deploy-pages` from 4 to 5
Release notes

Sourced from actions/deploy-pages's releases.

v5.0.0

Changelog


See details of all code changes since previous release.

:warning: For use with products other than GitHub.com, such as GitHub Enterprise Server, please consult the compatibility table.

v4.0.5

Changelog


See details of all code changes since previous release.

:warning: For use with products other than GitHub.com, such as GitHub Enterprise Server, please consult the compatibility table.

v4.0.4

Changelog


See details of all code changes since previous release.

:warning: For use with products other than GitHub.com, such as GitHub Enterprise Server, please consult the compatibility table.

v4.0.3

Changelog

... (truncated)

Commits
  • cd2ce8f Merge pull request #404 from salmanmkc/node24
  • bbe2a95 Update Node.js version to 24.x
  • 854d7aa Merge pull request #374 from actions/Jcambass-patch-1
  • 306bb81 Add workflow file for publishing releases to immutable action package
  • b742728 Merge pull request #360 from actions/dependabot/npm_and_yarn/npm_and_yarn-513...
  • 7273294 Bump braces in the npm_and_yarn group across 1 directory
  • 963791f Merge pull request #361 from actions/dependabot-friendly
  • 51bb29d Make the rebuild dist workflow safer for Dependabot
  • 89f3d10 Merge pull request #358 from actions/dependabot/npm_and_yarn/non-breaking-cha...
  • bce7355 Merge branch 'main' into dependabot/npm_and_yarn/non-breaking-changes-99c12deb21
  • Additional commits viewable in compare view

Updates `codecov/codecov-action` from 5 to 6
Release notes

Sourced from codecov/codecov-action's releases.

v6.0.0

⚠️ This version introduces support for node24 which make cause breaking changes for systems that do not currently support node24. ⚠️

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.5.4...v6.0.0

v5.5.4

This is a mirror of v5.5.2. v6 will be released which requires node24

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.5.3...v5.5.4

v5.5.3

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.5.2...v5.5.3

v5.5.2

What's Changed

New Contributors

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.5.1...v5.5.2

v5.5.1

What's Changed

... (truncated)

Changelog

Sourced from codecov/codecov-action's changelog.

v5.5.2

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.5.1..v5.5.2

v5.5.1

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.5.0..v5.5.1

v5.5.0

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.4.3..v5.5.0

v5.4.3

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.4.2..v5.4.3

v5.4.2

... (truncated)

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/apidocs.yaml | 4 ++-- .github/workflows/build.yml | 10 +++++----- .github/workflows/codeql.yml | 2 +- .github/workflows/conformance.yml | 8 ++++---- .github/workflows/gradle-publish.yml | 2 +- .github/workflows/samples.yml | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/apidocs.yaml b/.github/workflows/apidocs.yaml index 5c7ed4cd8..76f4434a2 100644 --- a/.github/workflows/apidocs.yaml +++ b/.github/workflows/apidocs.yaml @@ -34,7 +34,7 @@ jobs: cache: gradle - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 - name: Generate Dokka Site run: |- @@ -47,4 +47,4 @@ jobs: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4fd25226c..3467db243 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,7 +63,7 @@ jobs: run: npm ci - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: validate-wrappers: true add-job-summary: 'always' @@ -106,7 +106,7 @@ jobs: - name: Upload coverage reports to Codecov if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: coverage @@ -115,7 +115,7 @@ jobs: - name: Upload test results to Codecov if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} report_type: test_results @@ -151,7 +151,7 @@ jobs: distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: validate-wrappers: true add-job-summary: 'always' @@ -193,7 +193,7 @@ jobs: distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: add-job-summary: 'always' cache-read-only: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 41d1fdea9..5697b61b5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,7 +40,7 @@ jobs: build-mode: manual - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: validate-wrappers: true cache-read-only: true diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index b218b4ca1..5b5e405f9 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -34,7 +34,7 @@ jobs: distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: cache-read-only: ${{ github.ref != 'refs/heads/main' }} @@ -85,7 +85,7 @@ jobs: distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: cache-read-only: ${{ github.ref != 'refs/heads/main' }} @@ -116,7 +116,7 @@ jobs: distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: cache-read-only: ${{ github.ref != 'refs/heads/main' }} @@ -155,7 +155,7 @@ jobs: distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: cache-read-only: ${{ github.ref != 'refs/heads/main' }} diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index ed9a63a23..6b60e8739 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -25,7 +25,7 @@ jobs: distribution: 'temurin' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 - name: Build run: ./gradlew build --rerun-tasks --max-workers 2 --continue diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 632b71039..2c910fddb 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -45,7 +45,7 @@ jobs: distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: validate-wrappers: true add-job-summary: 'always' From 0e1eef32809d14c7c55337f0357138d05c0a55a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:38:30 +0000 Subject: [PATCH 043/187] chore(deps): bump the all-dependencies group across 1 directory with 4 updates (#627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-dependencies group with 4 updates in the /integration-test/src/jvmTest/typescript directory: [zod](https://github.com/colinhacks/zod), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [cors](https://github.com/expressjs/cors) and [typescript](https://github.com/microsoft/TypeScript). Updates `zod` from 4.3.5 to 4.3.6
Release notes

Sourced from zod's releases.

v4.3.6

Commits:

  • 9977fb0868432461de265a773319e80a90ba3e37 Add brand.dev to sponsors
  • f4b7bae3468f6188b8f004e007d722148fc91d77 Update pullfrog.yml (#5634)
  • 251d7163a0ac7740fee741428d913e3c55702ace Clean up workflow_call
  • edd4132466da0f5065a8e051b599d01fdd1081d8 fix: add missing User-agent to robots.txt and allow all (#5646)
  • 85db85e9091d0706910d60c7eb2e9c181edd87bd fix: typo in codec.test.ts file (#5628)
  • cbf77bb12bdfda2e054818e79001f5cb3798ce76 Avoid non null assertion (#5638)
  • dfbbf1c1ae0c224b8131d80ddf0a264262144086 Avoid re-exported star modules (#5656)
  • 762e911e5773f949452fd6dd4e360f2362110e8e Generalize numeric key handling
  • ca3c8629c0c2715571f70b44c2433cad3db7fe4e v4.3.6
Commits

Updates `@types/node` from 24.10.6 to 25.5.0
Commits

Updates `cors` from 2.8.5 to 2.8.6
Release notes

Sourced from cors's releases.

v2.8.6

What's Changed

New Contributors

... (truncated)

Changelog

Sourced from cors's changelog.

2.8.6 / 2026-01-22

  • Improve documentation (API, context, examples...)
  • Remove additional markdown files from tarball
Commits
Maintainer changes

This version was pushed to npm by ulisesgascon, a new releaser for cors since your current version.


Updates `typescript` from 5.9.3 to 6.0.2
Release notes

Sourced from typescript's releases.

TypeScript 6.0

For release notes, check out the release announcement blog post.

Downloads are available on:

TypeScript 6.0 Beta

For release notes, check out the release announcement.

Downloads are available on:

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/jvmTest/typescript/package-lock.json | 44 ++++++++++--------- .../src/jvmTest/typescript/package.json | 8 ++-- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 1f26245f7..2021be562 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -10,15 +10,15 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.28.0", "express": "^5.2.1", - "zod": "^4.3.5" + "zod": "^4.3.6" }, "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^24.10.6", - "cors": "^2.8.5", + "@types/node": "^25.5.0", + "cors": "^2.8.6", "tsx": "^4.21.0", - "typescript": "^5.9.3" + "typescript": "^6.0.2" } }, "node_modules/@esbuild/aix-ppc64": { @@ -579,13 +579,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.6.tgz", - "integrity": "sha512-B8h60xgJMR/xmgyX9fncRzEW9gCxoJjdenUhke2v1JGOd/V66KopmWrLPXi5oUI4VuiGK+d+HlXJjDRZMj21EQ==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/qs": { @@ -772,9 +772,9 @@ } }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -782,6 +782,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cross-spawn": { @@ -1733,9 +1737,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1747,9 +1751,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -1793,9 +1797,9 @@ "license": "ISC" }, "node_modules/zod": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", - "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index 3ce560cb0..0a43cd229 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -11,14 +11,14 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.28.0", "express": "^5.2.1", - "zod": "^4.3.5" + "zod": "^4.3.6" }, "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^24.10.6", - "cors": "^2.8.5", + "@types/node": "^25.5.0", + "cors": "^2.8.6", "tsx": "^4.21.0", - "typescript": "^5.9.3" + "typescript": "^6.0.2" } } From 4a82d20192f7191497ecffcf1ee8e4ceaec0f278 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:58:15 +0000 Subject: [PATCH 044/187] chore(deps): bump the other-dependencies group across 4 directories with 4 updates (#634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the other-dependencies group with 3 updates in the /samples/kotlin-mcp-client directory: [gradle-wrapper](https://github.com/gradle/gradle), [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java) and [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 2 updates in the /samples/kotlin-mcp-server directory: [gradle-wrapper](https://github.com/gradle/gradle) and [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 2 updates in the /samples/simple-streamable-server directory: [gradle-wrapper](https://github.com/gradle/gradle) and [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 3 updates in the /samples/weather-stdio-server directory: [gradle-wrapper](https://github.com/gradle/gradle), [com.gradleup.shadow](https://github.com/GradleUp/shadow) and [io.github.oshai:kotlin-logging-jvm](https://github.com/oshai/kotlin-logging). Updates `gradle-wrapper` from 9.3.1 to 9.4.1
Release notes

Sourced from gradle-wrapper's releases.

9.4.1

The Gradle team is excited to announce Gradle 9.4.1.

Here are the highlights of this release:

  • Java 26 support
  • Non-class-based JVM tests
  • Enhanced console progress bar

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: akankshaa-00, Attila Kelemen, Björn Kautler, dblood, Dennis Rieks, duvvuvenkataramana, John Burns, Julian, kevinstembridge, Niels Doucet, Philip Wedemann, ploober, Richard Hernandez, Roberto Perez Alcolea, Sebastian Lövdahl, stephan2405, Stephane Landelle, Ujwal Suresh Vanjare, Victor Merkulov, Vincent Potuček, Vladimir Sitnikov.

Upgrade instructions

Switch your build to use Gradle 9.4.1 by updating your wrapper:

./gradlew wrapper --gradle-version=9.4.1 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

... (truncated)

Commits

Updates `com.anthropic:anthropic-java` from 2.15.0 to 2.18.0
Release notes

Sourced from com.anthropic:anthropic-java's releases.

v2.18.0

2.18.0 (2026-03-18)

Full Changelog: v2.17.0...v2.18.0

Features

Bug Fixes

  • client: allow updating header/query affecting fields in toBuilder() (81c22ee)

Chores

  • internal: tweak CI branches (34b9875)
  • internal: update retry delay tests (704bc83)

v2.17.0

2.17.0 (2026-03-16)

Full Changelog: v2.16.1...v2.17.0

Features

  • api: GA thinking-display-setting (422b461)

v2.16.1

2.16.1 (2026-03-11)

Full Changelog: v2.16.0...v2.16.1

Bug Fixes

  • client: correct "exactly one" check in JSON schema validator (#309) (6e58ba9)
  • tests: declare baseUrl field in RetryingHttpClientTest (#1003) (4546f83)

Chores

  • internal: codegen related update (7a3a942)
  • internal: codegen related update (a293049)

v2.16.0

2.16.0 (2026-03-06)

Full Changelog: v2.15.0...v2.16.0

... (truncated)

Changelog

Sourced from com.anthropic:anthropic-java's changelog.

2.18.0 (2026-03-18)

Full Changelog: v2.17.0...v2.18.0

Features

Bug Fixes

  • client: allow updating header/query affecting fields in toBuilder() (81c22ee)

Chores

  • internal: tweak CI branches (34b9875)
  • internal: update retry delay tests (704bc83)

2.17.0 (2026-03-16)

Full Changelog: v2.16.1...v2.17.0

Features

  • api: GA thinking-display-setting (422b461)

2.16.1 (2026-03-11)

Full Changelog: v2.16.0...v2.16.1

Bug Fixes

  • client: correct "exactly one" check in JSON schema validator (#309) (6e58ba9)
  • tests: declare baseUrl field in RetryingHttpClientTest (#1003) (4546f83)

Chores

  • internal: codegen related update (7a3a942)
  • internal: codegen related update (a293049)

2.16.0 (2026-03-06)

Full Changelog: v2.15.0...v2.16.0

Features

  • api: change array_format to brackets (f9eef6e)

... (truncated)

Commits
  • 54cac60 release: 2.18.0
  • 26250c8 feat(api): manual updates
  • be47fc3 feat(api): manual updates
  • 39bf190 fix(client): allow updating header/query affecting fields in toBuilder()
  • 06d97c6 chore(internal): update retry delay tests
  • be96a0c chore(internal): tweak CI branches
  • ce72582 release: 2.17.0
  • 97cadfc feat(api): GA thinking-display-setting
  • cb2f646 release: 2.16.1
  • aa13ec7 fix(tests): declare baseUrl field in RetryingHttpClientTest (#1003)
  • Additional commits viewable in compare view

Updates `com.gradleup.shadow` from 9.2.2 to 9.4.0
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.0

Added

  • Support Isolated Projects. (#1139)

Changed

  • Allow opting out of adding shadowJar into assemble lifecycle. (#1939)
    shadow {
    // Disable making `assemble` task depend on `shadowJar`. This is enabled
    by default.
      addShadowJarToAssembleLifecycle = false
    }
    
  • Stop catching ZipException when writing entries. (#1970)

Fixed

  • Fix interaction with Gradle artifact transforms. (#1345)
  • Fix skipStringConstants per-relocator behavior in mapName. (#1968)
  • Fix failing for non-existent class directories. (#1976)

9.3.2

Changed

  • Stop moving gradleApi dependency from api to compileOnly for Gradle 9.4+. (#1919)
  • Log warnings for duplicates in the final JAR. (#1931)

Fixed

  • Fix relocation patterns not included in task fingerprint. (#1933)

9.3.1

Fixed

  • Use ASM from jdependency embedded. (#1898)
    This fixes potential classpath conflicts when using Shadow with other plugins that also use ASM.

9.3.0

Added

  • Add PatternFilterableResourceTransformer to simplify pattern based ResourceTransformers. (#1849)
  • Expose patternSet of ServiceFileTransformer as public. (#1849)
  • Expose patternSet of ApacheLicenseResourceTransformer as public. (#1850)
  • Expose patternSet of ApacheNoticeResourceTransformer as public. (#1850)
  • Expose patternSet of PreserveFirstFoundResourceTransformer as public. (#1855)
  • Support overriding output path of ApacheNoticeResourceTransformer. (#1851)
  • Add new merge strategy Fail to PropertiesFileTransformer. (#1856)
  • Add FindResourceInClasspath task to help with debugging issues with merged duplicate resources. (#1860)
  • Add MergeLicenseResourceTransformer. (#1858)

... (truncated)

Commits
  • e51f3e7 Prepare version 9.4.0
  • cf531d4 Prefer parameterized logging (#1977)
  • 85ba389 Fix failing for non-existent class directories (#1976)
  • 3a24369 Update dependency org.apache.maven:maven-model to v3.9.14 (#1973)
  • 6e3dfb5 Use removePrefix instead of replace for multi-release prefix removal (#1972)
  • 49f8c31 Reuse most logic for writing entries (#1971)
  • ed47d0c Update README.md for clarity and formatting
  • d73d94a Stop catching ZipException when writing entries (#1970)
  • 0564cae Fix skipStringConstants per-relocator behavior: use continue instead of retur...
  • 8d995b2 Split relocators remapping (#1967)
  • Additional commits viewable in compare view

Updates `gradle-wrapper` from 9.3.1 to 9.4.1
Release notes

Sourced from gradle-wrapper's releases.

9.4.1

The Gradle team is excited to announce Gradle 9.4.1.

Here are the highlights of this release:

  • Java 26 support
  • Non-class-based JVM tests
  • Enhanced console progress bar

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: akankshaa-00, Attila Kelemen, Björn Kautler, dblood, Dennis Rieks, duvvuvenkataramana, John Burns, Julian, kevinstembridge, Niels Doucet, Philip Wedemann, ploober, Richard Hernandez, Roberto Perez Alcolea, Sebastian Lövdahl, stephan2405, Stephane Landelle, Ujwal Suresh Vanjare, Victor Merkulov, Vincent Potuček, Vladimir Sitnikov.

Upgrade instructions

Switch your build to use Gradle 9.4.1 by updating your wrapper:

./gradlew wrapper --gradle-version=9.4.1 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

... (truncated)

Commits

Updates `com.gradleup.shadow` from 9.2.2 to 9.4.0
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.0

Added

  • Support Isolated Projects. (#1139)

Changed

  • Allow opting out of adding shadowJar into assemble lifecycle. (#1939)
    shadow {
    // Disable making `assemble` task depend on `shadowJar`. This is enabled
    by default.
      addShadowJarToAssembleLifecycle = false
    }
    
  • Stop catching ZipException when writing entries. (#1970)

Fixed

  • Fix interaction with Gradle artifact transforms. (#1345)
  • Fix skipStringConstants per-relocator behavior in mapName. (#1968)
  • Fix failing for non-existent class directories. (#1976)

9.3.2

Changed

  • Stop moving gradleApi dependency from api to compileOnly for Gradle 9.4+. (#1919)
  • Log warnings for duplicates in the final JAR. (#1931)

Fixed

  • Fix relocation patterns not included in task fingerprint. (#1933)

9.3.1

Fixed

  • Use ASM from jdependency embedded. (#1898)
    This fixes potential classpath conflicts when using Shadow with other plugins that also use ASM.

9.3.0

Added

  • Add PatternFilterableResourceTransformer to simplify pattern based ResourceTransformers. (#1849)
  • Expose patternSet of ServiceFileTransformer as public. (#1849)
  • Expose patternSet of ApacheLicenseResourceTransformer as public. (#1850)
  • Expose patternSet of ApacheNoticeResourceTransformer as public. (#1850)
  • Expose patternSet of PreserveFirstFoundResourceTransformer as public. (#1855)
  • Support overriding output path of ApacheNoticeResourceTransformer. (#1851)
  • Add new merge strategy Fail to PropertiesFileTransformer. (#1856)
  • Add FindResourceInClasspath task to help with debugging issues with merged duplicate resources. (#1860)
  • Add MergeLicenseResourceTransformer. (#1858)

... (truncated)

Commits
  • e51f3e7 Prepare version 9.4.0
  • cf531d4 Prefer parameterized logging (#1977)
  • 85ba389 Fix failing for non-existent class directories (#1976)
  • 3a24369 Update dependency org.apache.maven:maven-model to v3.9.14 (#1973)
  • 6e3dfb5 Use removePrefix instead of replace for multi-release prefix removal (#1972)
  • 49f8c31 Reuse most logic for writing entries (#1971)
  • ed47d0c Update README.md for clarity and formatting
  • d73d94a Stop catching ZipException when writing entries (#1970)
  • 0564cae Fix skipStringConstants per-relocator behavior: use continue instead of retur...
  • 8d995b2 Split relocators remapping (#1967)
  • Additional commits viewable in compare view

Updates `gradle-wrapper` from 9.3.1 to 9.4.1
Release notes

Sourced from gradle-wrapper's releases.

9.4.1

The Gradle team is excited to announce Gradle 9.4.1.

Here are the highlights of this release:

  • Java 26 support
  • Non-class-based JVM tests
  • Enhanced console progress bar

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: akankshaa-00, Attila Kelemen, Björn Kautler, dblood, Dennis Rieks, duvvuvenkataramana, John Burns, Julian, kevinstembridge, Niels Doucet, Philip Wedemann, ploober, Richard Hernandez, Roberto Perez Alcolea, Sebastian Lövdahl, stephan2405, Stephane Landelle, Ujwal Suresh Vanjare, Victor Merkulov, Vincent Potuček, Vladimir Sitnikov.

Upgrade instructions

Switch your build to use Gradle 9.4.1 by updating your wrapper:

./gradlew wrapper --gradle-version=9.4.1 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

... (truncated)

Commits

Updates `com.gradleup.shadow` from 9.2.2 to 9.4.0
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.0

Added

  • Support Isolated Projects. (#1139)

Changed

  • Allow opting out of adding shadowJar into assemble lifecycle. (#1939)
    shadow {
    // Disable making `assemble` task depend on `shadowJar`. This is enabled
    by default.
      addShadowJarToAssembleLifecycle = false
    }
    
  • Stop catching ZipException when writing entries. (#1970)

Fixed

  • Fix interaction with Gradle artifact transforms. (#1345)
  • Fix skipStringConstants per-relocator behavior in mapName. (#1968)
  • Fix failing for non-existent class directories. (#1976)

9.3.2

Changed

  • Stop moving gradleApi dependency from api to compileOnly for Gradle 9.4+. (#1919)
  • Log warnings for duplicates in the final JAR. (#1931)

Fixed

  • Fix relocation patterns not included in task fingerprint. (#1933)

9.3.1

Fixed

  • Use ASM from jdependency embedded. (#1898)
    This fixes potential classpath conflicts when using Shadow with other plugins that also use ASM.

9.3.0

Added

  • Add PatternFilterableResourceTransformer to simplify pattern based ResourceTransformers. (#1849)
  • Expose patternSet of ServiceFileTransformer as public. (#1849)
  • Expose patternSet of ApacheLicenseResourceTransformer as public. (#1850)
  • Expose patternSet of ApacheNoticeResourceTransformer as public. (#1850)
  • Expose patternSet of PreserveFirstFoundResourceTransformer as public. (#1855)
  • Support overriding output path of ApacheNoticeResourceTransformer. (#1851)
  • Add new merge strategy Fail to PropertiesFileTransformer. (#1856)
  • Add FindResourceInClasspath task to help with debugging issues with merged duplicate resources. (#1860)
  • Add MergeLicenseResourceTransformer. (#1858)

... (truncated)

Commits
  • e51f3e7 Prepare version 9.4.0
  • cf531d4 Prefer parameterized logging (#1977)
  • 85ba389 Fix failing for non-existent class directories (#1976)
  • 3a24369 Update dependency org.apache.maven:maven-model to v3.9.14 (#1973)
  • 6e3dfb5 Use removePrefix instead of replace for multi-release prefix removal (#1972)
  • 49f8c31 Reuse most logic for writing entries (#1971)
  • ed47d0c Update README.md for clarity and formatting
  • d73d94a Stop catching ZipException when writing entries (#1970)
  • 0564cae Fix skipStringConstants per-relocator behavior: use continue instead of retur...
  • 8d995b2 Split relocators remapping (#1967)
  • Additional commits viewable in compare view

Updates `gradle-wrapper` from 9.3.1 to 9.4.1
Release notes

Sourced from gradle-wrapper's releases.

9.4.1

The Gradle team is excited to announce Gradle 9.4.1.

Here are the highlights of this release:

  • Java 26 support
  • Non-class-based JVM tests
  • Enhanced console progress bar

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: akankshaa-00, Attila Kelemen, Björn Kautler, dblood, Dennis Rieks, duvvuvenkataramana, John Burns, Julian, kevinstembridge, Niels Doucet, Philip Wedemann, ploober, Richard Hernandez, Roberto Perez Alcolea, Sebastian Lövdahl, stephan2405, Stephane Landelle, Ujwal Suresh Vanjare, Victor Merkulov, Vincent Potuček, Vladimir Sitnikov.

Upgrade instructions

Switch your build to use Gradle 9.4.1 by updating your wrapper:

./gradlew wrapper --gradle-version=9.4.1 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

... (truncated)

Commits

Updates `com.gradleup.shadow` from 9.2.2 to 9.4.0
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.0

Added

  • Support Isolated Projects. (#1139)

Changed

  • Allow opting out of adding shadowJar into assemble lifecycle. (#1939)
    shadow {
    // Disable making `assemble` task depend on `shadowJar`. This is enabled
    by default.
      addShadowJarToAssembleLifecycle = false
    }
    
  • Stop catching ZipException when writing entries. (#1970)

Fixed

  • Fix interaction with Gradle artifact transforms. (#1345)
  • Fix skipStringConstants per-relocator behavior in mapName. (#1968)
  • Fix failing for non-existent class directories. (#1976)

9.3.2

Changed

  • Stop moving gradleApi dependency from api to compileOnly for Gradle 9.4+. (#1919)
  • Log warnings for duplicates in the final JAR. (#1931)

Fixed

  • Fix relocation patterns not included in task fingerprint. (#1933)

9.3.1

Fixed

  • Use ASM from jdependency embedded. (#1898)
    This fixes potential classpath conflicts when using Shadow with other plugins that also use ASM.

9.3.0

Added

  • Add PatternFilterableResourceTransformer to simplify pattern based ResourceTransformers. (#1849)
  • Expose patternSet of ServiceFileTransformer as public. (#1849)
  • Expose patternSet of ApacheLicenseResourceTransformer as public. (#1850)
  • Expose patternSet of ApacheNoticeResourceTransformer as public. (#1850)
  • Expose patternSet of PreserveFirstFoundResourceTransformer as public. (#1855)
  • Support overriding output path of ApacheNoticeResourceTransformer. (#1851)
  • Add new merge strategy Fail to PropertiesFileTransformer. (#1856)
  • Add FindResourceInClasspath task to help with debugging issues with merged duplicate resources. (#1860)
  • Add MergeLicenseResourceTransformer. (#1858)

... (truncated)

Commits
  • e51f3e7 Prepare version 9.4.0
  • cf531d4 Prefer parameterized logging (#1977)
  • 85ba389 Fix failing for non-existent class directories (#1976)
  • 3a24369 Update dependency org.apache.maven:maven-model to v3.9.14 (#1973)
  • 6e3dfb5 Use removePrefix instead of replace for multi-release prefix removal (#1972)
  • 49f8c31 Reuse most logic for writing entries (#1971)
  • ed47d0c Update README.md for clarity and formatting
  • d73d94a Stop catching ZipException when writing entries (#1970)
  • 0564cae Fix skipStringConstants per-relocator behavior: use continue instead of retur...
  • 8d995b2 Split relocators remapping (#1967)
  • Additional commits viewable in compare view

Updates `io.github.oshai:kotlin-logging-jvm` from 7.0.13 to 8.0.01
Release notes

Sourced from io.github.oshai:kotlin-logging-jvm's releases.

8.0.01

What's Changed

Full Changelog: https://github.com/oshai/kotlin-logging/compare/7.0.14...8.0.01

8.0.00-beta-04

What's Changed

Full Changelog: https://github.com/oshai/kotlin-logging/compare/8.0.00-beta-03...8.0.00-beta-04

8.0.00-beta-03

What's Changed

Full Changelog: https://github.com/oshai/kotlin-logging/compare/8.0.00-beta-02...8.0.00-beta-03

8.0.00-beta-02

What's Changed

Full Changelog: https://github.com/oshai/kotlin-logging/compare/8.0.00-beta-01...8.0.00-beta-02

8.0.00-beta-01

What's Changed

Full Changelog: https://github.com/oshai/kotlin-logging/compare/7.0.14...8.0.00-beta-01

7.0.14

What's Changed

... (truncated)

Commits
  • b1eb9fc bump version to 8.0.01
  • ff436aa bump version to 8.0.00-beta-05
  • 36e56c3 Fix Darwin OSLog composition failure in historical logs (#594)
  • 4ff0b7d bump version to 8.0.00-beta-04
  • 283f30c Feature: Add JPMS Support (Issue #365) (#592)
  • e5366c2 bump version to 8.0.00-beta-03
  • eb267ab feat: add configuration to suppress startup message
  • 6a67a6f Fix Darwin historical logs redaction using public format string
  • aa51a0d feat: add timestamp to KLoggingEvent
  • 733deaa fix action
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../gradle/libs.versions.toml | 4 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 46175 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/kotlin-mcp-client/gradlew | 2 +- samples/kotlin-mcp-client/gradlew.bat | 186 +++++++++--------- .../gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 46175 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/kotlin-mcp-server/gradlew | 2 +- samples/kotlin-mcp-server/gradlew.bat | 186 +++++++++--------- .../gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 46175 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/simple-streamable-server/gradlew | 2 +- samples/simple-streamable-server/gradlew.bat | 186 +++++++++--------- .../gradle/libs.versions.toml | 4 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 46175 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/weather-stdio-server/gradlew | 2 +- samples/weather-stdio-server/gradlew.bat | 186 +++++++++--------- 20 files changed, 386 insertions(+), 386 deletions(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index ab34b7e65..2a819cf46 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -anthropic = "2.15.0" +anthropic = "2.18.0" kotlin = "2.2.21" ktor = "3.2.3" mcp-kotlin = "0.9.0" -shadow = "9.2.2" +shadow = "9.4.0" slf4j = "2.0.17" [libraries] diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar index 61285a659d17295f1de7c53e24fdf13ad755c379..d997cfc60f4cff0e7451d19d49a82fa986695d07 100644 GIT binary patch delta 39855 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

SdO`o?U^bCPz6Ehh!k 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


Xdyd*p!l1Y@nCA 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 diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties index 37f78a6af..c61a118f7 100644 --- a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/samples/kotlin-mcp-client/gradlew b/samples/kotlin-mcp-client/gradlew index adff685a0..739907dfd 100755 --- a/samples/kotlin-mcp-client/gradlew +++ b/samples/kotlin-mcp-client/gradlew @@ -57,7 +57,7 @@ # 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 +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/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/. diff --git a/samples/kotlin-mcp-client/gradlew.bat b/samples/kotlin-mcp-client/gradlew.bat index c4bdd3ab8..e509b2dd8 100644 --- a/samples/kotlin-mcp-client/gradlew.bat +++ b/samples/kotlin-mcp-client/gradlew.bat @@ -1,93 +1,93 @@ -@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 - - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -: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 +@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 + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index 0ce6d5826..c96f6c164 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.2.21" ktor = "3.2.3" mcp-kotlin = "0.8.1" slf4j = "2.0.17" -shadow = "9.2.2" +shadow = "9.4.0" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar index 61285a659d17295f1de7c53e24fdf13ad755c379..d997cfc60f4cff0e7451d19d49a82fa986695d07 100644 GIT binary patch delta 39855 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

SdO`o?U^bCPz6Ehh!k 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


Xdyd*p!l1Y@nCA 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 diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties index 37f78a6af..c61a118f7 100644 --- a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/samples/kotlin-mcp-server/gradlew b/samples/kotlin-mcp-server/gradlew index adff685a0..739907dfd 100755 --- a/samples/kotlin-mcp-server/gradlew +++ b/samples/kotlin-mcp-server/gradlew @@ -57,7 +57,7 @@ # 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 +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/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/. diff --git a/samples/kotlin-mcp-server/gradlew.bat b/samples/kotlin-mcp-server/gradlew.bat index c4bdd3ab8..e509b2dd8 100644 --- a/samples/kotlin-mcp-server/gradlew.bat +++ b/samples/kotlin-mcp-server/gradlew.bat @@ -1,93 +1,93 @@ -@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 - - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -: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 +@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 + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index 7240ff23c..ebe741cf1 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.2.21" ktor = "3.2.3" mcp-kotlin = "0.9.0" slf4j = "2.0.17" -shadow = "9.2.2" +shadow = "9.4.0" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.jar b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.jar index 61285a659d17295f1de7c53e24fdf13ad755c379..d997cfc60f4cff0e7451d19d49a82fa986695d07 100644 GIT binary patch delta 39855 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

SdO`o?U^bCPz6Ehh!k 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


Xdyd*p!l1Y@nCA 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 diff --git a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties index 37f78a6af..c61a118f7 100644 --- a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/samples/simple-streamable-server/gradlew b/samples/simple-streamable-server/gradlew index adff685a0..739907dfd 100755 --- a/samples/simple-streamable-server/gradlew +++ b/samples/simple-streamable-server/gradlew @@ -57,7 +57,7 @@ # 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 +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/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/. diff --git a/samples/simple-streamable-server/gradlew.bat b/samples/simple-streamable-server/gradlew.bat index c4bdd3ab8..e509b2dd8 100644 --- a/samples/simple-streamable-server/gradlew.bat +++ b/samples/simple-streamable-server/gradlew.bat @@ -1,93 +1,93 @@ -@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 - - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -: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 +@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 + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index dab7f7d29..a580416aa 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -2,9 +2,9 @@ coroutines = "1.10.2" kotlin = "2.2.21" ktor = "3.2.3" -logging = "7.0.13" +logging = "8.0.01" mcp-kotlin = "0.9.0" -shadow = "9.2.2" +shadow = "9.4.0" slf4j = "2.0.17" [libraries] diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar index 61285a659d17295f1de7c53e24fdf13ad755c379..d997cfc60f4cff0e7451d19d49a82fa986695d07 100644 GIT binary patch delta 39855 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

SdO`o?U^bCPz6Ehh!k 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


Xdyd*p!l1Y@nCA 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 diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties index 37f78a6af..c61a118f7 100644 --- a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/samples/weather-stdio-server/gradlew b/samples/weather-stdio-server/gradlew index adff685a0..739907dfd 100755 --- a/samples/weather-stdio-server/gradlew +++ b/samples/weather-stdio-server/gradlew @@ -57,7 +57,7 @@ # 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 +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/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/. diff --git a/samples/weather-stdio-server/gradlew.bat b/samples/weather-stdio-server/gradlew.bat index c4bdd3ab8..e509b2dd8 100644 --- a/samples/weather-stdio-server/gradlew.bat +++ b/samples/weather-stdio-server/gradlew.bat @@ -1,93 +1,93 @@ -@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 - - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -: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 +@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 + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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 From 2b7e754bf38170280b4ed25aa7001d59640e22ff Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Fri, 27 Mar 2026 02:25:21 +0100 Subject: [PATCH 045/187] chore: remove deprecated macosX64 target (#625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove `macosX64()` target from multiplatform configuration ## Motivation and Context `macosX64` is [deprecated in Kotlin 2.2](https://kotlinlang.org/docs/whatsnew2220.html#deprecation-of-x86-64-apple-targets) and scheduled for removal in [2.3](https://kotlinlang.org/docs/native-target-support.html#deprecated-targets) ## How Has This Been Tested? — ## Breaking Changes for users of macosX64, no longer get a native `macosX64` artifact ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts index f8d89c0d9..a3311394c 100644 --- a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts +++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts @@ -52,7 +52,6 @@ kotlin { } } } - macosX64() macosArm64() linuxX64() linuxArm64() From b946e4eaf995324035524e434ac44897c86a9494 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:59:26 +0000 Subject: [PATCH 046/187] chore(deps): bump mcp-kotlin from 0.8.1 to 0.9.0 in /samples/kotlin-mcp-server (#638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.8.1 to 0.9.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.8.1 to 0.9.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.9.0

What's Changed

Breaking Changes

  • Server call handlers now receive a RequestContext — handler lambdas for tools, prompts, and resources have a new context parameter providing access to request metadata (#515) by @​rnett
  • Ktor extension functions moved from Routing to Route — update call sites: routing { mcpSse() } → route("/mcp") { mcpSse() } or use directly in any Route block (#531) by @​kpavlov
  • StdioServerTransport rethrows CancellationException — cancellation is no longer silently swallowed; ReadBuffer parsing was also fixed (#571) by @​kpavlov

New Features

  • kotlin-sdk-testing module — new artifact providing an in-process, channel-based TestClientTransport / TestServerTransport for unit testing MCP servers and clients without network I/O (#505) by @​kpavlov
  • StreamableHttpServerTransport.Configuration — configurable builder for the Streamable HTTP server transport (timeouts, session handling, etc.) (#560) by @​kpavlov
  • Ktor extensions for Streamable HTTP — mcpStreamableHttp() Ktor plugin and routing DSL for mounting a Streamable HTTP MCP server (#504 by @​devcrocod, #568 by @​kpavlov)
  • $defs support in ToolSchema — JSON Schema $defs / definitions can now be expressed in tool input schemas (#526) by @​i1bro

Bug Fixes

  • Fixed SSE transport endpoint resolution for non-root mount paths (#565) by @​Amaneusz
  • Fixed StdioClientTransport hanging indefinitely by adding proper read timeouts (#528) by @​kpavlov

Improvements

  • Introduced AbstractClientTransport base class, simplifying client transport lifecycle management (#516) by @​kpavlov
  • Deprecation cycle started — annotated APIs scheduled for removal in a future release (#498) by @​devcrocod
  • Detekt static analysis integrated into CI (#493, #507) by @​kpavlov
  • Code coverage reporting added via Codecov (#579, #580) by @​kpavlov
  • Flaky StdioClientTransport tests replaced with real stdin pipe–based tests (#508) by @​kpavlov

Dependency Updates

  • Kotest 6.1.2, MockK 1.14.9, AtomicFU 0.31.0, Gradle 9.3.1, Kover 0.9.7

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.8.4...0.9.0

0.8.4

Changes

... (truncated)

Commits
  • 5b58814 feat: bump version to 0.9.0 (#576)
  • 1454229 test: add serialization tests and guidelines for handling null and omitted ...
  • 3e7ea39 chore: relax Detekt rules for tests (#582)
  • a388180 fix!(server): #575 Rethrow CancellationException in StdioServerTransport, fix...
  • 9eabb54 refactor: refactor Streamable HTTP ktor extensions (#562) (#568)
  • 66d31d3 ci: update Codecov configuration for test and coverage upload (#580)
  • eb94f14 chore(deps): bump dev.mokksy:mokksy from 0.6.2 to 0.8.0 (#557)
  • c368812 ci: add Codecov coverage upload and XML report generation (#579)
  • 45fcc17 doc: update API reference URL in README.md (#578)
  • e124262 chore(deps): bump actions/download-artifact from 7 to 8
  • Additional commits viewable in compare view

Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.8.1 to 0.9.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

0.9.0

What's Changed

Breaking Changes

  • Server call handlers now receive a RequestContext — handler lambdas for tools, prompts, and resources have a new context parameter providing access to request metadata (#515) by @​rnett
  • Ktor extension functions moved from Routing to Route — update call sites: routing { mcpSse() } → route("/mcp") { mcpSse() } or use directly in any Route block (#531) by @​kpavlov
  • StdioServerTransport rethrows CancellationException — cancellation is no longer silently swallowed; ReadBuffer parsing was also fixed (#571) by @​kpavlov

New Features

  • kotlin-sdk-testing module — new artifact providing an in-process, channel-based TestClientTransport / TestServerTransport for unit testing MCP servers and clients without network I/O (#505) by @​kpavlov
  • StreamableHttpServerTransport.Configuration — configurable builder for the Streamable HTTP server transport (timeouts, session handling, etc.) (#560) by @​kpavlov
  • Ktor extensions for Streamable HTTP — mcpStreamableHttp() Ktor plugin and routing DSL for mounting a Streamable HTTP MCP server (#504 by @​devcrocod, #568 by @​kpavlov)
  • $defs support in ToolSchema — JSON Schema $defs / definitions can now be expressed in tool input schemas (#526) by @​i1bro

Bug Fixes

  • Fixed SSE transport endpoint resolution for non-root mount paths (#565) by @​Amaneusz
  • Fixed StdioClientTransport hanging indefinitely by adding proper read timeouts (#528) by @​kpavlov

Improvements

  • Introduced AbstractClientTransport base class, simplifying client transport lifecycle management (#516) by @​kpavlov
  • Deprecation cycle started — annotated APIs scheduled for removal in a future release (#498) by @​devcrocod
  • Detekt static analysis integrated into CI (#493, #507) by @​kpavlov
  • Code coverage reporting added via Codecov (#579, #580) by @​kpavlov
  • Flaky StdioClientTransport tests replaced with real stdin pipe–based tests (#508) by @​kpavlov

Dependency Updates

  • Kotest 6.1.2, MockK 1.14.9, AtomicFU 0.31.0, Gradle 9.3.1, Kover 0.9.7

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.8.4...0.9.0

0.8.4

Changes

... (truncated)

Commits
  • 5b58814 feat: bump version to 0.9.0 (#576)
  • 1454229 test: add serialization tests and guidelines for handling null and omitted ...
  • 3e7ea39 chore: relax Detekt rules for tests (#582)
  • a388180 fix!(server): #575 Rethrow CancellationException in StdioServerTransport, fix...
  • 9eabb54 refactor: refactor Streamable HTTP ktor extensions (#562) (#568)
  • 66d31d3 ci: update Codecov configuration for test and coverage upload (#580)
  • eb94f14 chore(deps): bump dev.mokksy:mokksy from 0.6.2 to 0.8.0 (#557)
  • c368812 ci: add Codecov coverage upload and XML report generation (#579)
  • 45fcc17 doc: update API reference URL in README.md (#578)
  • e124262 chore(deps): bump actions/download-artifact from 7 to 8
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index c96f6c164..72b8a1649 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.2.21" ktor = "3.2.3" -mcp-kotlin = "0.8.1" +mcp-kotlin = "0.9.0" slf4j = "2.0.17" shadow = "9.4.0" From 0d97b8296de16968215f1c722ef5a6ab87325004 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:38:17 +0000 Subject: [PATCH 047/187] chore(deps): bump the kotest group with 3 updates (#629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the kotest group with 3 updates: [io.kotest:kotest-assertions-core](https://github.com/kotest/kotest), [io.kotest:kotest-assertions-json](https://github.com/kotest/kotest) and [io.kotest:kotest-assertions-ktor](https://github.com/kotest/kotest). Updates `io.kotest:kotest-assertions-core` from 6.1.3 to 6.1.9
Release notes

Sourced from io.kotest:kotest-assertions-core's releases.

v6.1.9

Full Changelog: https://github.com/kotest/kotest/compare/v6.1.8...v6.1.9

The Gradle 9.4 --tests filter change was missed from the 6.1.8 release. This 6.1.9 release includes it.

If you are using Gradle 9.4+ then you need this build to run a single test from the IDE.

v6.1.8

What's Changed

New Contributors

Full Changelog: https://github.com/kotest/kotest/compare/v6.1.7...v6.1.8

v6.1.7

... (truncated)

Commits
  • b368573 Revert the Gradle 9.4 test filter handling removal
  • 666ae51 Simplify discovery log message formatting in Discovery.kt.
  • f641e00 Add primitive array support for containAll matcher (#4354) (#5776)
  • 2d21515 Add primitive array support for inorder (shouldContainInOrder) matcher (#4354...
  • 8f7a507 Increase forNone performance threshold to 10x for CI variance
  • 92ee8ff Normalize line breaks in test names for Gradle test filters (#5764)
  • 359f65e Add tests and fix for afterSpec/beforeSpec in TestFactory (#4133) (#5786)
  • 4ee4a97 Add finalizeSpec DSL function to TestConfiguration and Spec (#5750)
  • e0a45fa Add primitive array support for beIn (shouldBeIn) matcher (#4354) (#5780)
  • 282a929 Add LazyJvmAssertion to avoid stacktraces in inspectors (#5759)
  • Additional commits viewable in compare view

Updates `io.kotest:kotest-assertions-json` from 6.1.3 to 6.1.9
Release notes

Sourced from io.kotest:kotest-assertions-json's releases.

v6.1.9

Full Changelog: https://github.com/kotest/kotest/compare/v6.1.8...v6.1.9

The Gradle 9.4 --tests filter change was missed from the 6.1.8 release. This 6.1.9 release includes it.

If you are using Gradle 9.4+ then you need this build to run a single test from the IDE.

v6.1.8

What's Changed

New Contributors

Full Changelog: https://github.com/kotest/kotest/compare/v6.1.7...v6.1.8

v6.1.7

... (truncated)

Commits
  • b368573 Revert the Gradle 9.4 test filter handling removal
  • 666ae51 Simplify discovery log message formatting in Discovery.kt.
  • f641e00 Add primitive array support for containAll matcher (#4354) (#5776)
  • 2d21515 Add primitive array support for inorder (shouldContainInOrder) matcher (#4354...
  • 8f7a507 Increase forNone performance threshold to 10x for CI variance
  • 92ee8ff Normalize line breaks in test names for Gradle test filters (#5764)
  • 359f65e Add tests and fix for afterSpec/beforeSpec in TestFactory (#4133) (#5786)
  • 4ee4a97 Add finalizeSpec DSL function to TestConfiguration and Spec (#5750)
  • e0a45fa Add primitive array support for beIn (shouldBeIn) matcher (#4354) (#5780)
  • 282a929 Add LazyJvmAssertion to avoid stacktraces in inspectors (#5759)
  • Additional commits viewable in compare view

Updates `io.kotest:kotest-assertions-ktor` from 6.1.3 to 6.1.9
Release notes

Sourced from io.kotest:kotest-assertions-ktor's releases.

v6.1.9

Full Changelog: https://github.com/kotest/kotest/compare/v6.1.8...v6.1.9

The Gradle 9.4 --tests filter change was missed from the 6.1.8 release. This 6.1.9 release includes it.

If you are using Gradle 9.4+ then you need this build to run a single test from the IDE.

v6.1.8

What's Changed

New Contributors

Full Changelog: https://github.com/kotest/kotest/compare/v6.1.7...v6.1.8

v6.1.7

... (truncated)

Commits
  • b368573 Revert the Gradle 9.4 test filter handling removal
  • 666ae51 Simplify discovery log message formatting in Discovery.kt.
  • f641e00 Add primitive array support for containAll matcher (#4354) (#5776)
  • 2d21515 Add primitive array support for inorder (shouldContainInOrder) matcher (#4354...
  • 8f7a507 Increase forNone performance threshold to 10x for CI variance
  • 92ee8ff Normalize line breaks in test names for Gradle test filters (#5764)
  • 359f65e Add tests and fix for afterSpec/beforeSpec in TestFactory (#4133) (#5786)
  • 4ee4a97 Add finalizeSpec DSL function to TestConfiguration and Spec (#5750)
  • e0a45fa Add primitive array support for beIn (shouldBeIn) matcher (#4354) (#5780)
  • 282a929 Add LazyJvmAssertion to avoid stacktraces in inspectors (#5759)
  • Additional commits viewable in compare view

Updates `io.kotest:kotest-assertions-json` from 6.1.3 to 6.1.9
Release notes

Sourced from io.kotest:kotest-assertions-json's releases.

v6.1.9

Full Changelog: https://github.com/kotest/kotest/compare/v6.1.8...v6.1.9

The Gradle 9.4 --tests filter change was missed from the 6.1.8 release. This 6.1.9 release includes it.

If you are using Gradle 9.4+ then you need this build to run a single test from the IDE.

v6.1.8

What's Changed

New Contributors

Full Changelog: https://github.com/kotest/kotest/compare/v6.1.7...v6.1.8

v6.1.7

... (truncated)

Commits
  • b368573 Revert the Gradle 9.4 test filter handling removal
  • 666ae51 Simplify discovery log message formatting in Discovery.kt.
  • f641e00 Add primitive array support for containAll matcher (#4354) (#5776)
  • 2d21515 Add primitive array support for inorder (shouldContainInOrder) matcher (#4354...
  • 8f7a507 Increase forNone performance threshold to 10x for CI variance
  • 92ee8ff Normalize line breaks in test names for Gradle test filters (#5764)
  • 359f65e Add tests and fix for afterSpec/beforeSpec in TestFactory (#4133) (#5786)
  • 4ee4a97 Add finalizeSpec DSL function to TestConfiguration and Spec (#5750)
  • e0a45fa Add primitive array support for beIn (shouldBeIn) matcher (#4354) (#5780)
  • 282a929 Add LazyJvmAssertion to avoid stacktraces in inspectors (#5759)
  • Additional commits viewable in compare view

Updates `io.kotest:kotest-assertions-ktor` from 6.1.3 to 6.1.9
Release notes

Sourced from io.kotest:kotest-assertions-ktor's releases.

v6.1.9

Full Changelog: https://github.com/kotest/kotest/compare/v6.1.8...v6.1.9

The Gradle 9.4 --tests filter change was missed from the 6.1.8 release. This 6.1.9 release includes it.

If you are using Gradle 9.4+ then you need this build to run a single test from the IDE.

v6.1.8

What's Changed

New Contributors

Full Changelog: https://github.com/kotest/kotest/compare/v6.1.7...v6.1.8

v6.1.7

... (truncated)

Commits
  • b368573 Revert the Gradle 9.4 test filter handling removal
  • 666ae51 Simplify discovery log message formatting in Discovery.kt.
  • f641e00 Add primitive array support for containAll matcher (#4354) (#5776)
  • 2d21515 Add primitive array support for inorder (shouldContainInOrder) matcher (#4354...
  • 8f7a507 Increase forNone performance threshold to 10x for CI variance
  • 92ee8ff Normalize line breaks in test names for Gradle test filters (#5764)
  • 359f65e Add tests and fix for afterSpec/beforeSpec in TestFactory (#4133) (#5786)
  • 4ee4a97 Add finalizeSpec DSL function to TestConfiguration and Spec (#5750)
  • e0a45fa Add primitive array support for beIn (shouldBeIn) matcher (#4354) (#5780)
  • 282a929 Add LazyJvmAssertion to avoid stacktraces in inspectors (#5759)
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Pavel Gorgulov --- 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 a3cb75984..bf56c878e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ openapi-generator = "7.21.0" awaitility = "4.3.0" collections-immutable = "0.4.0" coroutines = "1.10.2" -kotest = "6.1.3" +kotest = "6.1.9" kotlinx-io = "0.8.2" ktor = "3.2.3" logging = "8.0.01" From e7687c79393a6e9174bda3bfb610b3ed1a5f5d53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:58:25 +0000 Subject: [PATCH 048/187] chore(deps): bump the testing group across 1 directory with 2 updates (#630) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the testing group with 2 updates in the / directory: [dev.mokksy:mokksy](https://github.com/mokksy/mokksy) and [org.junit.jupiter:junit-jupiter-params](https://github.com/junit-team/junit-framework). Updates `dev.mokksy:mokksy` from 0.8.0 to 0.8.1
Release notes

Sourced from dev.mokksy:mokksy's releases.

0.8.1

What's Changed

Bug Fixes

  • Header chaining fixed — multiple headers { } blocks now accumulate correctly instead of the last one silently overwriting earlier ones by @​kpavlov in mokksy/mokksy#29
  • Improved exception handling in streaming responses — errors during stream building are now logged with context and rethrown cleanly.

Improvements

  • Unified header API — headers and headerList direct properties replaced by a single HeadersConfigurer DSL for response header configuration by @​kpavlov in mokksy/mokksy#29
  • Priority API cleaned up — priority is now non-nullable with a sensible default; redundant priority() method removed by @​kpavlov in mokksy/mokksy#29
  • Faster stub lookup — StubRegistry now uses binary search for stub matching by @​kpavlov in mokksy/mokksy#29

Dependencies

Full Changelog: https://github.com/mokksy/mokksy/compare/v0.8.0...v0.8.1

Commits
  • 1be47c4 release: finalize version 0.8.1
  • 3e8ebea docs: create Module.md and update Dokka includes
  • 5a48038 tests: simplify StringOrListSerializerTest assertions
  • e7bc227 docs: add explanation and example for stub priority in README
  • 861ec28 fix: ensure headersLambda chaining in ResponseDefinitionBuilders
  • f00d03c tests: improve StringOrListSerializerTest with shouldEqualJson assertions
  • af59bb9 tests: add @BeforeEach to reset mocks in MokksyServerJavaTest
  • f988b68 refactor: use binarySearch in StubRegistry for optimized stub lookup
  • a5f45e1 tests: enhance header validation and error handling in response builder tests
  • ff8de25 Update mokksy/src/jvmTest/kotlin/dev/mokksy/mokksy/serializers/StringOrListSe...
  • Additional commits viewable in compare view

Updates `org.junit.jupiter:junit-jupiter-params` from 5.14.2 to 5.14.3
Release notes

Sourced from org.junit.jupiter:junit-jupiter-params's releases.

JUnit 5.14.3 = Platform 1.14.3 + Jupiter 5.14.3 + Vintage 5.14.3

See Release Notes.

Full Changelog: https://github.com/junit-team/junit-framework/compare/r5.14.2...r5.14.3

Commits
  • 1c14c1b Release 5.14.3
  • 698391f Finalize 5.14.3 release notes
  • 5655c22 Fix link to milestone
  • b4d1f56 Reliably support JRE.OTHER with @⁠EnabledOnJre and @⁠DisabledOnJre
  • 5b6dfef Fix references
  • 718ee15 Fail build if xref fragment is invalid
  • a809887 Install poppler-utils for pdfinfo
  • b568f5a Update API baseline
  • 1ebb6bc Add missing checkout step
  • 4ca615b Back to snapshots for further development
  • See full diff in compare view

Most Recent Ignore Conditions Applied to This Pull Request | Dependency Name | Ignore Conditions | | --- | --- | | org.junit.jupiter:junit-jupiter-params | [>= 6.a, < 7] |
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf56c878e..f9160e5e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,10 +21,10 @@ kotlinx-io = "0.8.2" ktor = "3.2.3" logging = "8.0.01" mockk = "1.14.9" -mokksy = "0.8.0" +mokksy = "0.8.1" serialization = "1.9.0" slf4j = "2.0.17" -junit = "5.14.2" +junit = "5.14.3" [libraries] # Plugins From 9c0aa45e2aaf130a9eba2ff20f12db8bdc8f4725 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Fri, 27 Mar 2026 16:16:10 +0100 Subject: [PATCH 049/187] fix: reject duplicate initialize requests in ServerSession (#624) `ServerSession.handleInitialize()` now rejects duplicate `initialize` requests with `INVALID_REQUEST` (-32600) error. `clientCapabilities` and `clientVersion` changed from `var` to `AtomicRef`-backed `val`, with atomic `compareAndSet(null, ...)` serving as both the guard and the write ## How Has This Been Tested? second `initialize` returns error, original capabilities preserved ## Breaking Changes none ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context --- .../kotlin/sdk/client/ClientTest.kt | 9 +- .../sdk/server/ServerSessionInitializeTest.kt | 152 ++++++++++++++++++ .../kotlin/sdk/server/ServerSession.kt | 21 ++- 3 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index b117dbfb4..9d512361e 100644 --- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -417,13 +417,14 @@ class ClientTest { clientInfo = Implementation(name = "test client without capability", version = "1.0"), options = ClientOptions( capabilities = ClientCapabilities(), - // enforceStrictCapabilities = true // TODO() ), ) - clientWithoutCapability.connect(clientTransport) - // Using the same transport pair might not be realistic - in a real scenario you'd create another pair. - // Adjust if necessary. + val (clientTransport2, serverTransport2) = InMemoryTransport.createLinkedPair() + listOf( + launch { clientWithoutCapability.connect(clientTransport2) }, + launch { server.createSession(serverTransport2) }, + ).joinAll() // This should fail val ex = assertFailsWith { diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt new file mode 100644 index 000000000..5031a9c06 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt @@ -0,0 +1,152 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import org.junit.jupiter.api.Test +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ServerSessionInitializeTest { + + private fun createSession(): ServerSession = ServerSession( + serverInfo = Implementation(name = "test-server", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + instructions = null, + ) + + private fun createInitializeRequest(clientName: String = "test-client"): InitializeRequest = InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = clientName, version = "1.0"), + ), + ) + + @Test + fun `should handle first initialize request successfully`() = runTest { + val session = createSession() + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + assertNull(session.clientCapabilities) + assertNull(session.clientVersion) + + val responseDone = CompletableDeferred() + clientTransport.onMessage { message -> + if (message is JSONRPCResponse) { + responseDone.complete(message) + } + } + + session.connect(serverTransport) + clientTransport.send(createInitializeRequest().toJSON()) + + val response = responseDone.await() + assertNotNull(response.result) + assertNotNull(session.clientCapabilities) + assertEquals("test-client", session.clientVersion?.name) + } + + @Test + fun `should reject duplicate initialize request`() = runTest { + val session = createSession() + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val responses = CopyOnWriteArrayList() + val secondResponseDone = CompletableDeferred() + + clientTransport.onMessage { message -> + when (message) { + is JSONRPCResponse, is JSONRPCError -> { + responses.add(message) + if (responses.size == 2) secondResponseDone.complete(Unit) + } + + else -> {} + } + } + + session.connect(serverTransport) + + // First initialize should succeed + clientTransport.send(createInitializeRequest(clientName = "first-client").toJSON()) + + // Second initialize should be rejected + clientTransport.send(createInitializeRequest(clientName = "second-client").toJSON()) + + secondResponseDone.await() + + assertEquals(2, responses.size) + assertTrue(responses[0] is JSONRPCResponse, "First response should be success") + assertTrue(responses[1] is JSONRPCError, "Second response should be error") + assertEquals(RPCError.ErrorCode.INVALID_REQUEST, (responses[1] as JSONRPCError).error.code) + + // Capabilities still reflect the first client, not overwritten + assertEquals("first-client", session.clientVersion?.name) + } + + @Test + fun `should reject concurrent initialize requests - only first succeeds`() = runTest { + val session = createSession() + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val n = 10 + val allResponsesDone = CompletableDeferred() + val successes = CopyOnWriteArrayList() + val errors = CopyOnWriteArrayList() + + clientTransport.onMessage { message -> + when (message) { + is JSONRPCResponse -> successes.add(message) + is JSONRPCError -> errors.add(message) + else -> {} + } + if (successes.size + errors.size == n) { + allResponsesDone.complete(Unit) + } + } + + session.connect(serverTransport) + + // Use Dispatchers.Default for true parallelism on JVM + withContext(Dispatchers.Default) { + val barrier = CompletableDeferred() + val jobs = (1..n).map { i -> + launch { + barrier.await() + clientTransport.send( + createInitializeRequest(clientName = "client-$i").toJSON(), + ) + } + } + barrier.complete(Unit) + jobs.joinAll() + } + + allResponsesDone.await() + + assertEquals(1, successes.size, "Exactly one initialize should succeed") + assertEquals(n - 1, errors.size, "All other initializes should be rejected") + errors.forEach { error -> + assertEquals(RPCError.ErrorCode.INVALID_REQUEST, error.error.code) + } + } +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt index 69d20d154..a8a34aff3 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -20,8 +20,10 @@ import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.McpException import io.modelcontextprotocol.kotlin.sdk.types.Method import io.modelcontextprotocol.kotlin.sdk.types.Method.Defined +import io.modelcontextprotocol.kotlin.sdk.types.RPCError import io.modelcontextprotocol.kotlin.sdk.types.RequestMeta import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS @@ -52,17 +54,18 @@ public open class ServerSession( private var _onClose: () -> Unit = {} + private val _clientCapabilities: AtomicRef = atomic(null) + private val _clientVersion: AtomicRef = atomic(null) + /** * The client's reported capabilities after initialization. */ - public var clientCapabilities: ClientCapabilities? = null - private set + public val clientCapabilities: ClientCapabilities? get() = _clientCapabilities.value /** * The client's version information after initialization. */ - public var clientVersion: Implementation? = null - private set + public val clientVersion: Implementation? get() = _clientVersion.value /** * The capabilities supported by the server, related to the session. @@ -286,9 +289,15 @@ public open class ServerSession( } private fun handleInitialize(request: InitializeRequest): InitializeResult { + if (!_clientCapabilities.compareAndSet(null, request.params.capabilities)) { + throw McpException( + code = RPCError.ErrorCode.INVALID_REQUEST, + message = "Server already initialized", + ) + } + logger.debug { "Handling initialization request from client" } - clientCapabilities = request.params.capabilities - clientVersion = request.params.clientInfo + _clientVersion.value = request.params.clientInfo val requestedVersion = request.params.protocolVersion val protocolVersion = if (SUPPORTED_PROTOCOL_VERSIONS.contains(requestedVersion)) { From d60b0a29c1b9a55fcbbdab9f81b2d6d33c30472d Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Fri, 27 Mar 2026 18:37:26 +0100 Subject: [PATCH 050/187] chore: #608 update kotlin and kotlinx versions (#611) bumps: - kotlin 2.2.21 -> 2.3.10 - ktor 3.2.3 -> 3.3.3 - kotlinx-io 0.8.2 -> 0.9.0 - kotlinx-serialization 1.9.0 -> 1.10.0 ## Breaking Changes ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context Unfortunately, CodeQL does not yet support the latest version of Kotlin Colses #608 --- gradle/libs.versions.toml | 8 +- gradlew.bat | 186 +++++++++++++++++++------------------- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9160e5e2..0ee94320a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ atomicfu = "0.31.0" binaryCompatibilityValidatorPlugin = "0.18.1" detekt = "2.0.0-alpha.2" dokka = "2.2.0" -kotlin = "2.2.21" +kotlin = "2.3.10" kover = "0.9.8" ktlint = "14.2.0" knit = "0.5.1" @@ -17,12 +17,12 @@ awaitility = "4.3.0" collections-immutable = "0.4.0" coroutines = "1.10.2" kotest = "6.1.9" -kotlinx-io = "0.8.2" -ktor = "3.2.3" +kotlinx-io = "0.9.0" +ktor = "3.3.3" logging = "8.0.01" mockk = "1.14.9" mokksy = "0.8.1" -serialization = "1.9.0" +serialization = "1.10.0" slf4j = "2.0.17" junit = "5.14.3" diff --git a/gradlew.bat b/gradlew.bat index e509b2dd8..c4bdd3ab8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,93 +1,93 @@ -@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 - - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -: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 +@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 + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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 From 0042c745af87d40736fe77bde00accdf0a709026 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:23:29 +0000 Subject: [PATCH 051/187] chore(deps): bump org.jetbrains.kotlinx:atomicfu-gradle-plugin from 0.31.0 to 0.32.1 in the kotlinx group across 1 directory (#644) Bumps the kotlinx group with 1 update in the / directory: [org.jetbrains.kotlinx:atomicfu-gradle-plugin](https://github.com/Kotlin/kotlinx.atomicfu). Updates `org.jetbrains.kotlinx:atomicfu-gradle-plugin` from 0.31.0 to 0.32.1
Release notes

Sourced from org.jetbrains.kotlinx:atomicfu-gradle-plugin's releases.

0.32.1

0.32.1 is identical to 0.32.0 and it only includes infra updates required to publish the Gradle plugin to Gradle Plugin Portal.

0.32.0

  • SynchronizedObject implementation was reverted to version 0.29.0 to resolve a performance regression (See #584 for details)

[!NOTE] AtomicFU Gradle plugin with version 0.32.0 was not published in the Gradle Plugin Portal.

Changelog

Sourced from org.jetbrains.kotlinx:atomicfu-gradle-plugin's changelog.

Version 0.32.1

  • Fixed issues with the Gradle plugin publication to the Gradle plugin portal (#589)

Version 0.32.0

  • SynchronizedObject implementation was reverted to version 0.29.0 to resolve a performance regression (See #584 for details)
Commits

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 0ee94320a..f511d0d56 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] # plugins version -atomicfu = "0.31.0" +atomicfu = "0.32.1" binaryCompatibilityValidatorPlugin = "0.18.1" detekt = "2.0.0-alpha.2" dokka = "2.2.0" From f10e47531689a248251f00e4aaaafab7a4f5b5bf Mon Sep 17 00:00:00 2001 From: Prachi Pandey Date: Mon, 30 Mar 2026 11:21:11 +0530 Subject: [PATCH 052/187] refactor: move enterprise auth to standalone kotlin-sdk-client-enterprise-auth module --- kotlin-sdk-client-enterprise-auth/Module.md | 52 ++++ .../api/kotlin-sdk-client-enterprise-auth.api | 235 ++++++++++++++++++ .../build.gradle.kts | 49 ++++ .../sdk/client/auth/AuthServerMetadata.kt | 0 .../DiscoverAndRequestJwtAuthGrantOptions.kt | 0 .../kotlin/sdk/client/auth/EnterpriseAuth.kt | 0 .../auth/EnterpriseAuthAssertionContext.kt | 0 .../client/auth/EnterpriseAuthException.kt | 0 .../sdk/client/auth/EnterpriseAuthProvider.kt | 0 .../auth/EnterpriseAuthProviderOptions.kt | 0 .../auth/ExchangeJwtBearerGrantOptions.kt | 0 .../client/auth/JagTokenExchangeResponse.kt | 0 .../auth/JwtBearerAccessTokenResponse.kt | 0 .../client/auth/RequestJwtAuthGrantOptions.kt | 0 .../client/auth/EnterpriseAuthProviderTest.kt | 0 .../sdk/client/auth/EnterpriseAuthTest.kt | 0 kotlin-sdk-client/api/kotlin-sdk-client.api | 235 ------------------ kotlin-sdk/build.gradle.kts | 1 + settings.gradle.kts | 1 + 19 files changed, 338 insertions(+), 235 deletions(-) create mode 100644 kotlin-sdk-client-enterprise-auth/Module.md create mode 100644 kotlin-sdk-client-enterprise-auth/api/kotlin-sdk-client-enterprise-auth.api create mode 100644 kotlin-sdk-client-enterprise-auth/build.gradle.kts rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt (100%) rename {kotlin-sdk-client => kotlin-sdk-client-enterprise-auth}/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt (100%) diff --git a/kotlin-sdk-client-enterprise-auth/Module.md b/kotlin-sdk-client-enterprise-auth/Module.md new file mode 100644 index 000000000..36f8e3921 --- /dev/null +++ b/kotlin-sdk-client-enterprise-auth/Module.md @@ -0,0 +1,52 @@ +# Module kotlin-sdk-client-enterprise-auth + +Enterprise Managed Authorization (SEP-990) support for Kotlin MCP clients. Implements the +two-step OAuth 2.0 flow — RFC 8693 token exchange for a JWT Authorization Grant followed by +RFC 7523 JWT Bearer grant exchange — as a standalone Ktor [HttpClientPlugin]. + +**Platform Support:** Multiplatform • Kotlin 2.2+ + +## Key Classes + +- [EnterpriseAuthProvider] — Ktor `HttpClientPlugin`; intercepts outgoing requests, performs + full enterprise auth flow, caches the resulting access token, and injects + `Authorization: Bearer` headers automatically + - [EnterpriseAuthProviderOptions] — configuration: `clientId`, `assertionCallback`, etc. + - [EnterpriseAuthAssertionContext] — context passed to the assertion callback +- [EnterpriseAuth] — low-level utility object with individual `suspend` functions for each + auth step; use when you need fine-grained control + - [AuthServerMetadata] — RFC 8414 discovery response model + - [RequestJwtAuthGrantOptions] / [DiscoverAndRequestJwtAuthGrantOptions] — Step 1 options + - [ExchangeJwtBearerGrantOptions] — Step 2 options + - [JagTokenExchangeResponse] / [JwtBearerAccessTokenResponse] — response models +- [EnterpriseAuthException] — thrown on any auth flow failure + +## Example + +```kotlin +val httpClient = HttpClient(CIO) { + install(SSE) + install(EnterpriseAuthProvider) { + clientId = "my-mcp-client" + assertionCallback = { ctx -> + EnterpriseAuth.requestJwtAuthorizationGrant( + RequestJwtAuthGrantOptions( + tokenEndpoint = "https://idp.example.com/token", + idToken = myIdTokenSupplier(), + clientId = "my-idp-client", + clientSecret = "idp-client-secret", + audience = ctx.authorizationServerUrl, + resource = ctx.resourceUrl, + ), + authHttpClient, + ) + } + } +} + +val transport = StreamableHttpClientTransport(client = httpClient, url = serverUrl) +``` + +# Package io.modelcontextprotocol.kotlin.sdk.client.auth + +Enterprise Managed Authorization types and utilities (SEP-990). diff --git a/kotlin-sdk-client-enterprise-auth/api/kotlin-sdk-client-enterprise-auth.api b/kotlin-sdk-client-enterprise-auth/api/kotlin-sdk-client-enterprise-auth.api new file mode 100644 index 000000000..8e9d898f2 --- /dev/null +++ b/kotlin-sdk-client-enterprise-auth/api/kotlin-sdk-client-enterprise-auth.api @@ -0,0 +1,235 @@ +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata; + public fun equals (Ljava/lang/Object;)Z + public final fun getAuthorizationEndpoint ()Ljava/lang/String; + public final fun getIssuer ()Ljava/lang/String; + public final fun getJwksUri ()Ljava/lang/String; + public final fun getTokenEndpoint ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata; + 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/client/auth/AuthServerMetadata;)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/client/auth/AuthServerMetadata$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getAudience ()Ljava/lang/String; + public final fun getClientId ()Ljava/lang/String; + public final fun getClientSecret ()Ljava/lang/String; + public final fun getIdToken ()Ljava/lang/String; + public final fun getIdpTokenEndpoint ()Ljava/lang/String; + public final fun getIdpUrl ()Ljava/lang/String; + public final fun getResource ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth { + public static final field GRANT_TYPE_JWT_BEARER Ljava/lang/String; + public static final field GRANT_TYPE_TOKEN_EXCHANGE Ljava/lang/String; + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth; + public static final field TOKEN_TYPE_ID_JAG Ljava/lang/String; + public static final field TOKEN_TYPE_ID_TOKEN Ljava/lang/String; + public final fun discoverAndRequestJwtAuthorizationGrant (Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun discoverAuthServerMetadata (Ljava/lang/String;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun exchangeJwtBearerGrant (Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun requestJwtAuthorizationGrant (Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun getAuthorizationServerUrl ()Ljava/lang/String; + public final fun getResourceUrl ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException : java/lang/RuntimeException { + public fun (Ljava/lang/String;Ljava/lang/Throwable;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider { + public static final field Plugin Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider$Plugin; + public fun (Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions;Lio/ktor/client/HttpClient;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions;Lio/ktor/client/HttpClient;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun invalidateCache ()V +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider$Config { + public fun ()V + public final fun getAssertionCallback ()Lkotlin/jvm/functions/Function2; + public final fun getAuthHttpClient ()Lio/ktor/client/HttpClient; + public final fun getClientId ()Ljava/lang/String; + public final fun getClientSecret ()Ljava/lang/String; + public final fun getExpiryBuffer-UwyO8pc ()J + public final fun getScope ()Ljava/lang/String; + public final fun setAssertionCallback (Lkotlin/jvm/functions/Function2;)V + public final fun setAuthHttpClient (Lio/ktor/client/HttpClient;)V + public final fun setClientId (Ljava/lang/String;)V + public final fun setClientSecret (Ljava/lang/String;)V + public final fun setExpiryBuffer-LRDsOJo (J)V + public final fun setScope (Ljava/lang/String;)V +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider$Plugin : io/ktor/client/plugins/HttpClientPlugin { + public fun getKey ()Lio/ktor/util/AttributeKey; + public fun install (Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider;Lio/ktor/client/HttpClient;)V + public synthetic fun install (Ljava/lang/Object;Lio/ktor/client/HttpClient;)V + public fun prepare (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider; + public synthetic fun prepare (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions { + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAssertionCallback ()Lkotlin/jvm/functions/Function2; + public final fun getClientId ()Ljava/lang/String; + public final fun getClientSecret ()Ljava/lang/String; + public final fun getExpiryBuffer-UwyO8pc ()J + public final fun getScope ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getAssertion ()Ljava/lang/String; + public final fun getClientId ()Ljava/lang/String; + public final fun getClientSecret ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; + public final fun getTokenEndpoint ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse; + public fun equals (Ljava/lang/Object;)Z + public final fun getAccessToken ()Ljava/lang/String; + public final fun getExpiresIn ()Ljava/lang/Integer; + public final fun getIssuedTokenType ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; + public final fun getTokenType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse; + 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/client/auth/JagTokenExchangeResponse;)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/client/auth/JagTokenExchangeResponse$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAccessToken ()Ljava/lang/String; + public final fun getExpiresAt ()Lkotlin/time/ComparableTimeMark; + public final fun getExpiresIn ()Ljava/lang/Integer; + public final fun getRefreshToken ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; + public final fun getTokenType ()Ljava/lang/String; + public final fun isExpired ()Z +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse; + 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/client/auth/JwtBearerAccessTokenResponse;)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/client/auth/JwtBearerAccessTokenResponse$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getAudience ()Ljava/lang/String; + public final fun getClientId ()Ljava/lang/String; + public final fun getClientSecret ()Ljava/lang/String; + public final fun getIdToken ()Ljava/lang/String; + public final fun getResource ()Ljava/lang/String; + public final fun getScope ()Ljava/lang/String; + public final fun getTokenEndpoint ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/kotlin-sdk-client-enterprise-auth/build.gradle.kts b/kotlin-sdk-client-enterprise-auth/build.gradle.kts new file mode 100644 index 000000000..d5a043366 --- /dev/null +++ b/kotlin-sdk-client-enterprise-auth/build.gradle.kts @@ -0,0 +1,49 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + id("mcp.multiplatform") + id("mcp.publishing") + id("mcp.dokka") + 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.ktor.client.core) + implementation(project(":kotlin-sdk-core")) + implementation(libs.kotlin.logging) + } + } + + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.kotest.assertions.core) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.ktor.client.mock) + } + } + } +} diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata.kt b/kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata.kt similarity index 100% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata.kt rename to kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata.kt diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt b/kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt similarity index 100% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt rename to kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions.kt diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt b/kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt similarity index 100% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt rename to kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth.kt diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext.kt b/kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext.kt similarity index 100% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext.kt rename to kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext.kt diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException.kt b/kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException.kt similarity index 100% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException.kt rename to kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException.kt diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt b/kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt similarity index 100% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt rename to kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider.kt diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt b/kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt similarity index 100% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt rename to kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions.kt diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt b/kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt similarity index 100% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt rename to kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions.kt diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt b/kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt similarity index 100% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt rename to kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse.kt diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt b/kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt similarity index 100% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt rename to kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse.kt diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt b/kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt similarity index 100% rename from kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt rename to kotlin-sdk-client-enterprise-auth/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions.kt diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt b/kotlin-sdk-client-enterprise-auth/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt similarity index 100% rename from kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt rename to kotlin-sdk-client-enterprise-auth/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderTest.kt diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt b/kotlin-sdk-client-enterprise-auth/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt similarity index 100% rename from kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt rename to kotlin-sdk-client-enterprise-auth/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthTest.kt diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index 0642ea2e3..da0e752a7 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -142,238 +142,3 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorCli 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/client/auth/AuthServerMetadata { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata$Companion; - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata; - public fun equals (Ljava/lang/Object;)Z - public final fun getAuthorizationEndpoint ()Ljava/lang/String; - public final fun getIssuer ()Ljava/lang/String; - public final fun getJwksUri ()Ljava/lang/String; - public final fun getTokenEndpoint ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/AuthServerMetadata; - 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/client/auth/AuthServerMetadata;)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/client/auth/AuthServerMetadata$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Ljava/lang/String; - public final fun component5 ()Ljava/lang/String; - public final fun component6 ()Ljava/lang/String; - public final fun component7 ()Ljava/lang/String; - public final fun component8 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions; - public fun equals (Ljava/lang/Object;)Z - public final fun getAudience ()Ljava/lang/String; - public final fun getClientId ()Ljava/lang/String; - public final fun getClientSecret ()Ljava/lang/String; - public final fun getIdToken ()Ljava/lang/String; - public final fun getIdpTokenEndpoint ()Ljava/lang/String; - public final fun getIdpUrl ()Ljava/lang/String; - public final fun getResource ()Ljava/lang/String; - public final fun getScope ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth { - public static final field GRANT_TYPE_JWT_BEARER Ljava/lang/String; - public static final field GRANT_TYPE_TOKEN_EXCHANGE Ljava/lang/String; - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuth; - public static final field TOKEN_TYPE_ID_JAG Ljava/lang/String; - public static final field TOKEN_TYPE_ID_TOKEN Ljava/lang/String; - public final fun discoverAndRequestJwtAuthorizationGrant (Lio/modelcontextprotocol/kotlin/sdk/client/auth/DiscoverAndRequestJwtAuthGrantOptions;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun discoverAuthServerMetadata (Ljava/lang/String;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun exchangeJwtBearerGrant (Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun requestJwtAuthorizationGrant (Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions;Lio/ktor/client/HttpClient;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthAssertionContext { - public fun (Ljava/lang/String;Ljava/lang/String;)V - public final fun getAuthorizationServerUrl ()Ljava/lang/String; - public final fun getResourceUrl ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthException : java/lang/RuntimeException { - public fun (Ljava/lang/String;Ljava/lang/Throwable;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider { - public static final field Plugin Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider$Plugin; - public fun (Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions;Lio/ktor/client/HttpClient;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions;Lio/ktor/client/HttpClient;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun invalidateCache ()V -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider$Config { - public fun ()V - public final fun getAssertionCallback ()Lkotlin/jvm/functions/Function2; - public final fun getAuthHttpClient ()Lio/ktor/client/HttpClient; - public final fun getClientId ()Ljava/lang/String; - public final fun getClientSecret ()Ljava/lang/String; - public final fun getExpiryBuffer-UwyO8pc ()J - public final fun getScope ()Ljava/lang/String; - public final fun setAssertionCallback (Lkotlin/jvm/functions/Function2;)V - public final fun setAuthHttpClient (Lio/ktor/client/HttpClient;)V - public final fun setClientId (Ljava/lang/String;)V - public final fun setClientSecret (Ljava/lang/String;)V - public final fun setExpiryBuffer-LRDsOJo (J)V - public final fun setScope (Ljava/lang/String;)V -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider$Plugin : io/ktor/client/plugins/HttpClientPlugin { - public fun getKey ()Lio/ktor/util/AttributeKey; - public fun install (Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider;Lio/ktor/client/HttpClient;)V - public synthetic fun install (Ljava/lang/Object;Lio/ktor/client/HttpClient;)V - public fun prepare (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProvider; - public synthetic fun prepare (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/EnterpriseAuthProviderOptions { - public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;JILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Ljava/lang/String;JLkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getAssertionCallback ()Lkotlin/jvm/functions/Function2; - public final fun getClientId ()Ljava/lang/String; - public final fun getClientSecret ()Ljava/lang/String; - public final fun getExpiryBuffer-UwyO8pc ()J - public final fun getScope ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Ljava/lang/String; - public final fun component5 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/ExchangeJwtBearerGrantOptions; - public fun equals (Ljava/lang/Object;)Z - public final fun getAssertion ()Ljava/lang/String; - public final fun getClientId ()Ljava/lang/String; - public final fun getClientSecret ()Ljava/lang/String; - public final fun getScope ()Ljava/lang/String; - public final fun getTokenEndpoint ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse$Companion; - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Ljava/lang/String; - public final fun component5 ()Ljava/lang/Integer; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse; - public fun equals (Ljava/lang/Object;)Z - public final fun getAccessToken ()Ljava/lang/String; - public final fun getExpiresIn ()Ljava/lang/Integer; - public final fun getIssuedTokenType ()Ljava/lang/String; - public final fun getScope ()Ljava/lang/String; - public final fun getTokenType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JagTokenExchangeResponse; - 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/client/auth/JagTokenExchangeResponse;)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/client/auth/JagTokenExchangeResponse$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse$Companion; - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getAccessToken ()Ljava/lang/String; - public final fun getExpiresAt ()Lkotlin/time/ComparableTimeMark; - public final fun getExpiresIn ()Ljava/lang/Integer; - public final fun getRefreshToken ()Ljava/lang/String; - public final fun getScope ()Ljava/lang/String; - public final fun getTokenType ()Ljava/lang/String; - public final fun isExpired ()Z -} - -public final synthetic class io/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/JwtBearerAccessTokenResponse; - 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/client/auth/JwtBearerAccessTokenResponse;)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/client/auth/JwtBearerAccessTokenResponse$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - -public final class io/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Ljava/lang/String; - public final fun component5 ()Ljava/lang/String; - public final fun component6 ()Ljava/lang/String; - public final fun component7 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/client/auth/RequestJwtAuthGrantOptions; - public fun equals (Ljava/lang/Object;)Z - public final fun getAudience ()Ljava/lang/String; - public final fun getClientId ()Ljava/lang/String; - public final fun getClientSecret ()Ljava/lang/String; - public final fun getIdToken ()Ljava/lang/String; - public final fun getResource ()Ljava/lang/String; - public final fun getScope ()Ljava/lang/String; - public final fun getTokenEndpoint ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - diff --git a/kotlin-sdk/build.gradle.kts b/kotlin-sdk/build.gradle.kts index e59df60e0..cafd6b08d 100644 --- a/kotlin-sdk/build.gradle.kts +++ b/kotlin-sdk/build.gradle.kts @@ -9,6 +9,7 @@ kotlin { dependencies { api(project(":kotlin-sdk-core")) api(project(":kotlin-sdk-client")) + api(project(":kotlin-sdk-client-enterprise-auth")) api(project(":kotlin-sdk-server")) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 60e3d1cd3..e123866b6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,7 @@ include( ":test-utils", ":kotlin-sdk-core", ":kotlin-sdk-client", + ":kotlin-sdk-client-enterprise-auth", ":kotlin-sdk-server", ":kotlin-sdk-testing", ":kotlin-sdk", From d5337d4234c6ae891d8a34709222a1554c12443d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:43:53 +0300 Subject: [PATCH 053/187] chore(deps): bump modelcontextprotocol/conformance from 0.1.15 to 0.1.16 in the all-actions group (#657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-actions group with 1 update: [modelcontextprotocol/conformance](https://github.com/modelcontextprotocol/conformance). Updates `modelcontextprotocol/conformance` from 0.1.15 to 0.1.16
Release notes

Sourced from modelcontextprotocol/conformance's releases.

v0.1.16

What's Changed

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/conformance/compare/v0.1.15...v0.1.16

Commits
  • 21a9a2f chore: bump version to 0.1.16 (#198)
  • 1c48a30 feat: add --spec-version option to tier-check (#176)
  • fe16e38 fix: don't leak escalated scopes in PRM scopes_supported for step-up test (#154)
  • a2ec2d4 Fix Windows compatibility in tier-check client conformance (#168)
  • e702a25 fix: resolve CLI path for tier-check conformance subprocess (#175)
  • 7b5865a chore(deps-dev): bump the all-dependencies group with 2 updates (#196)
  • 687d5b4 chore(deps): ignore major version bumps in dependabot (#195)
  • 4859a81 docs: add AGENTS.md and CONTRIBUTING.md (#194)
  • 8f3994c chore(deps): consolidate dependabot updates (#192)
  • 02a5f1c feat (auth): add sep-2207 client checks (#166)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=modelcontextprotocol/conformance&package-manager=github_actions&previous-version=0.1.15&new-version=0.1.16)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/conformance.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 5b5e405f9..4058d02cc 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -53,7 +53,7 @@ jobs: done - name: Run conformance tests - uses: modelcontextprotocol/conformance@v0.1.15 + uses: modelcontextprotocol/conformance@v0.1.16 with: mode: server url: http://localhost:3001/mcp @@ -93,7 +93,7 @@ jobs: run: ./gradlew :conformance-test:installDist - name: Run conformance tests - uses: modelcontextprotocol/conformance@v0.1.15 + uses: modelcontextprotocol/conformance@v0.1.16 with: mode: client command: conformance-test/build/install/conformance-test/bin/conformance-client @@ -124,7 +124,7 @@ jobs: run: ./gradlew :conformance-test:installDist - name: Run conformance tests - uses: modelcontextprotocol/conformance@v0.1.15 + uses: modelcontextprotocol/conformance@v0.1.16 with: mode: client command: conformance-test/build/install/conformance-test/bin/conformance-client @@ -163,7 +163,7 @@ jobs: run: ./gradlew :conformance-test:installDist - name: Run conformance tests - uses: modelcontextprotocol/conformance@v0.1.15 + uses: modelcontextprotocol/conformance@v0.1.16 with: mode: client command: conformance-test/build/install/conformance-test/bin/conformance-client From 7ed57f9fc7f1cdfcaff3f68e7a7cad2ad9112146 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:18:06 +0300 Subject: [PATCH 054/187] chore(deps): bump mcp-kotlin from 0.9.0 to 0.10.0 in /samples/weather-stdio-server (#654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.9.0 to 0.10.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.9.0 to 0.10.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.10.0

Highlights

This release adds MCP conformance testing infrastructure, SSE reconnection support, Tasks protocol types, resource template matching, and
tool execution properties. It also completes a deprecation cycle — removing all previously deprecated (error-level) symbols from io.modelcontextprotocol.kotlin.sdk in favor of their replacements in io.modelcontextprotocol.kotlin.sdk.types.

Breaking Changes

  • Deprecation cycle completed — All symbols deprecated at ERROR level in 0.9.0 have been removed by @​devcrocod (#615). This includes
    the entire io.modelcontextprotocol.kotlin.sdk.types.kt compatibility file (type aliases such as CallToolResult, CancelledNotification, ClientCapabilities, CompleteRequest, etc.) and the legacy McpJson in io.modelcontextprotocol.kotlin.sdk. Migrate to the equivalents in io.modelcontextprotocol.kotlin.sdk.types.
  • SSE/Streamable HTTP transport changes — StreamableHttpClientTransport now accepts ReconnectionOptions for configurable retry behavior by @​devcrocod (#585, #596). The previous constructor signatures remain available.

Features

  • MCP conformance test infrastructure — Comprehensive conformance test suite covering core protocol operations, tool calls, elicitation, resources, prompts, and 20 OAuth/auth scenarios. Includes CI workflow, baseline tracking, and shell runner by @​devcrocod (#585)
  • SSE reconnection with retry support — Client transports now support configurable reconnection with exponential backoff via
    ReconnectionOptions (initial delay, max delay, multiplier, max retries) by @​devcrocod (#596)
  • Tasks protocol types — Added Task, TaskStatus, and related models per the MCP tasks specification by @​devcrocod (#566)
  • Resource templates with basic template matcher — Server-side support for URI template-based resource matching by @​kpavlov (#502, #606)
  • Tool execution properties — Tool now exposes execution properties for richer tool metadata by @​devcrocod (#567)

Bug Fixes

  • Notifications not delivered over Streamable HTTP — Fixed server-side notification delivery for Streamable HTTP transport sessions by @​kpavlov (#587, #599)
  • Conformance: SEP-1330 enum schemas — Corrected elicitation enum schemas in conformance tests by @​kpavlov (#600)

Documentation

Maintenance

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.9.0...0.10.0

Commits
  • 5ad0492 chore: bump project version to 0.10.0 in gradle.properties
  • bd4cf0c docs: add missing KDoc comments for public functions, properties, and classes...
  • c77ebe0 refactor(core): Fix Kotlin language and api version to 2.1 in build config (#...
  • cd91eed chore: simplify and clean up CI workflows (#617)
  • 435698a refactor(integration-test): modernize ts tests, update versions, rename SSE -...
  • 5583990 chore: deprecation cycle (#615)
  • 55afb63 chore(deps): bump gradle-wrapper from 9.3.1 to 9.4.1 (#614)
  • fd3a858 feat: add tasks models (#566)
  • 62e4e50 docs: update AGENTS.md (#612)
  • ee6561c feat: #502 resource templates with basic template matcher (#606)
  • Additional commits viewable in compare view

Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.9.0 to 0.10.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

0.10.0

Highlights

This release adds MCP conformance testing infrastructure, SSE reconnection support, Tasks protocol types, resource template matching, and
tool execution properties. It also completes a deprecation cycle — removing all previously deprecated (error-level) symbols from io.modelcontextprotocol.kotlin.sdk in favor of their replacements in io.modelcontextprotocol.kotlin.sdk.types.

Breaking Changes

  • Deprecation cycle completed — All symbols deprecated at ERROR level in 0.9.0 have been removed by @​devcrocod (#615). This includes
    the entire io.modelcontextprotocol.kotlin.sdk.types.kt compatibility file (type aliases such as CallToolResult, CancelledNotification, ClientCapabilities, CompleteRequest, etc.) and the legacy McpJson in io.modelcontextprotocol.kotlin.sdk. Migrate to the equivalents in io.modelcontextprotocol.kotlin.sdk.types.
  • SSE/Streamable HTTP transport changes — StreamableHttpClientTransport now accepts ReconnectionOptions for configurable retry behavior by @​devcrocod (#585, #596). The previous constructor signatures remain available.

Features

  • MCP conformance test infrastructure — Comprehensive conformance test suite covering core protocol operations, tool calls, elicitation, resources, prompts, and 20 OAuth/auth scenarios. Includes CI workflow, baseline tracking, and shell runner by @​devcrocod (#585)
  • SSE reconnection with retry support — Client transports now support configurable reconnection with exponential backoff via
    ReconnectionOptions (initial delay, max delay, multiplier, max retries) by @​devcrocod (#596)
  • Tasks protocol types — Added Task, TaskStatus, and related models per the MCP tasks specification by @​devcrocod (#566)
  • Resource templates with basic template matcher — Server-side support for URI template-based resource matching by @​kpavlov (#502, #606)
  • Tool execution properties — Tool now exposes execution properties for richer tool metadata by @​devcrocod (#567)

Bug Fixes

  • Notifications not delivered over Streamable HTTP — Fixed server-side notification delivery for Streamable HTTP transport sessions by @​kpavlov (#587, #599)
  • Conformance: SEP-1330 enum schemas — Corrected elicitation enum schemas in conformance tests by @​kpavlov (#600)

Documentation

Maintenance

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.9.0...0.10.0

Commits
  • 5ad0492 chore: bump project version to 0.10.0 in gradle.properties
  • bd4cf0c docs: add missing KDoc comments for public functions, properties, and classes...
  • c77ebe0 refactor(core): Fix Kotlin language and api version to 2.1 in build config (#...
  • cd91eed chore: simplify and clean up CI workflows (#617)
  • 435698a refactor(integration-test): modernize ts tests, update versions, rename SSE -...
  • 5583990 chore: deprecation cycle (#615)
  • 55afb63 chore(deps): bump gradle-wrapper from 9.3.1 to 9.4.1 (#614)
  • fd3a858 feat: add tasks models (#566)
  • 62e4e50 docs: update AGENTS.md (#612)
  • ee6561c feat: #502 resource templates with basic template matcher (#606)
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index a580416aa..d1a647af8 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ coroutines = "1.10.2" kotlin = "2.2.21" ktor = "3.2.3" logging = "8.0.01" -mcp-kotlin = "0.9.0" +mcp-kotlin = "0.10.0" shadow = "9.4.0" slf4j = "2.0.17" From 384064ed43ebffffda66673d9dcd8eddd9a7cb55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:50:53 +0300 Subject: [PATCH 055/187] chore(deps): bump the other-dependencies group across 4 directories with 1 update (#648) Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-client directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 1 update in the /samples/simple-streamable-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 1 update in the /samples/weather-stdio-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow). Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Updates `com.gradleup.shadow` from 9.4.0 to 9.4.1
Release notes

Sourced from com.gradleup.shadow's releases.

9.4.1

Changed

  • Update Kotlin to 2.3.20. (#1978)
Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- samples/simple-streamable-server/gradle/libs.versions.toml | 2 +- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 2a819cf46..227422b8a 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -3,7 +3,7 @@ anthropic = "2.18.0" kotlin = "2.2.21" ktor = "3.2.3" mcp-kotlin = "0.9.0" -shadow = "9.4.0" +shadow = "9.4.1" slf4j = "2.0.17" [libraries] diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index 72b8a1649..fa0cd8197 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.2.21" ktor = "3.2.3" mcp-kotlin = "0.9.0" slf4j = "2.0.17" -shadow = "9.4.0" +shadow = "9.4.1" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index ebe741cf1..1f893bd67 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.2.21" ktor = "3.2.3" mcp-kotlin = "0.9.0" slf4j = "2.0.17" -shadow = "9.4.0" +shadow = "9.4.1" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index d1a647af8..0b75f097f 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -4,7 +4,7 @@ kotlin = "2.2.21" ktor = "3.2.3" logging = "8.0.01" mcp-kotlin = "0.10.0" -shadow = "9.4.0" +shadow = "9.4.1" slf4j = "2.0.17" [libraries] From c46f53a7bf49f6b9edb66af21227c46cfcffffbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:08:35 +0000 Subject: [PATCH 056/187] chore(deps): bump path-to-regexp from 8.3.0 to 8.4.0 in /integration-test/src/jvmTest/typescript (#647) Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 8.3.0 to 8.4.0.
Release notes

Sourced from path-to-regexp's releases.

8.4.0

Important

Fixed

Changed

  • Dedupes regex prefixes (pillarjs/path-to-regexp#422)
    • This will result in shorter regular expressions for some cases using optional groups
  • Rejects large optional route combinations (pillarjs/path-to-regexp#424)
    • When using groups such as /users{/delete} it will restrict the number of generated combinations to < 256, equivalent to 8 top-level optional groups and unlikely to occur in a real world application, but avoids exploding the regex size for applications that accept user created routes
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=path-to-regexp&package-manager=npm_and_yarn&previous-version=8.3.0&new-version=8.4.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/modelcontextprotocol/kotlin-sdk/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 2021be562..ca30f6578 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -1429,9 +1429,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", "license": "MIT", "funding": { "type": "opencollective", From 7a95cb7e19eb90216c040635156653034f407916 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:22:15 +0000 Subject: [PATCH 057/187] chore(deps): bump io.ktor:ktor-bom from 3.2.3 to 3.3.3 in /samples/kotlin-mcp-server (#652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [io.ktor:ktor-bom](https://github.com/ktorio/ktor) from 3.2.3 to 3.3.3.
Release notes

Sourced from io.ktor:ktor-bom's releases.

3.3.3

Published 26 November 2025

Improvements

  • KTOR-6837 Discrepancies when parsing URL host with CIO and Darwin engines compared to the rest engines
  • KTOR-9050 Logging: SimpleLogger should be an object, not a class
  • KTOR-9094 Jetty Client: Support HTTP/2 over cleartext (h2c)
  • KTOR-9120 OpenAPI gen: missing operationId for KDoc fields
  • KTOR-3019 Improve logging for CORS plugin

Bugfixes

  • KTOR-8671 Netty: RejectedExecutionException during shutdown on MacOS when dev mode is enabled
  • KTOR-9096 Darwin: New SSE handlers stop responding after canceling few SSE sessions
  • KTOR-9125 Double ResponseSent invocation when exception is thrown after respond
  • KTOR-8878 OpenAPI: StackOverflowError when a response object has property with @Contextual serializer
  • KTOR-8947 Java, ContentEncoding: IllegalHeaderNameException is thrown for ":status" pseudo header with HTTP/2
  • KTOR-9092 NettyHttp2Handler throws IllegalArgumentException: 'ktor.ApplicationCall' is already in use
  • KTOR-8924 Curl: Client sends both Transfer-Encoding and Content-Length headers for DELETE requests with body
  • KTOR-8838 Exception handling issue in client cache

3.3.2

Published 5 November 2025

Improvements

  • WebRTC Client. Remove redundant targets (KTOR-9062)
  • Add Socks proxy support to Darwin engine (KTOR-8968)
  • Java: Improve error message when SOCKS proxy is used (KTOR-2908)

Bugfixes

  • HttpRequestRetry: SendCountExceedException when max retries is more than maxSendCount of HttpSend (KTOR-5850)
  • Darwin: The maxFrameSize option has no effect (KTOR-6963)
  • OpenAPI: StackOverflowError when a response object has property with @Contextual serializer (KTOR-8878)
  • OpenAPI gen: missing KDoc fields (KTOR-9021)
  • Server call.request.path() returns routing selectors in path (KTOR-7639)
  • StaticContent doesn't allow siblings (KTOR-9012)
  • HttpCache: FileStorage doesn't use given dispatcher for all file operations (KTOR-8832)
  • Curl: SOCKS proxy doesn't work (KTOR-9008)
  • Netty: java.lang.VerifyError is thrown on Android since 3.3.0 (KTOR-8916)
  • Response body channel is canceled while the body is being saved when having HttpRequestRetry and onDownload (KTOR-8975)
  • HttpCache: InvalidCacheStateException when varyKeys stored in files contain uppercase letters since 3.3.0 (KTOR-8970)

3.3.1

Published 8 October 2025

Improvements

  • Add a note about SSE session lifetime in KDoc (KTOR-8440)
  • Update Kotlin to 2.2.20 (KTOR-8896)

Bugfixes

  • NumberFormatException when Content-Length header value contains null bytes (KTOR-4828)

... (truncated)

Changelog

Sourced from io.ktor:ktor-bom's changelog.

3.3.3

Published 26 November 2025

Improvements

  • KTOR-6837 Discrepancies when parsing URL host with CIO and Darwin engines compared to the rest engines
  • KTOR-9050 Logging: SimpleLogger should be an object, not a class
  • KTOR-9094 Jetty Client: Support HTTP/2 over cleartext (h2c)
  • KTOR-9120 OpenAPI gen: missing operationId for KDoc fields
  • KTOR-3019 Improve logging for CORS plugin

Bugfixes

  • KTOR-8671 Netty: RejectedExecutionException during shutdown on MacOS when dev mode is enabled
  • KTOR-9096 Darwin: New SSE handlers stop responding after canceling few SSE sessions
  • KTOR-9125 Double ResponseSent invocation when exception is thrown after respond
  • KTOR-8878 OpenAPI: StackOverflowError when a response object has property with @​Contextual serializer
  • KTOR-8947 Java, ContentEncoding: IllegalHeaderNameException is thrown for ":status" pseudo header with HTTP/2
  • KTOR-9092 NettyHttp2Handler throws IllegalArgumentException: 'ktor.ApplicationCall' is already in use
  • KTOR-8924 Curl: Client sends both Transfer-Encoding and Content-Length headers for DELETE requests with body
  • KTOR-8838 Exception handling issue in client cache

3.3.2

Published 5 November 2025

Improvements

  • WebRTC Client. Remove redundant targets (KTOR-9062)
  • Add Socks proxy support to Darwin engine (KTOR-8968)
  • Java: Improve error message when SOCKS proxy is used (KTOR-2908)

Bugfixes

  • HttpRequestRetry: SendCountExceedException when max retries is more than maxSendCount of HttpSend (KTOR-5850)
  • Darwin: The maxFrameSize option has no effect (KTOR-6963)
  • OpenAPI: StackOverflowError when a response object has property with @​Contextual serializer (KTOR-8878)
  • OpenAPI gen: missing KDoc fields (KTOR-9021)
  • Server call.request.path() returns routing selectors in path (KTOR-7639)
  • StaticContent doesn't allow siblings (KTOR-9012)
  • HttpCache: FileStorage doesn't use given dispatcher for all file operations (KTOR-8832)
  • Curl: SOCKS proxy doesn't work (KTOR-9008)
  • Netty: java.lang.VerifyError is thrown on Android since 3.3.0 (KTOR-8916)
  • Response body channel is canceled while the body is being saved when having HttpRequestRetry and onDownload (KTOR-8975)
  • HttpCache: InvalidCacheStateException when varyKeys stored in files contain uppercase letters since 3.3.0 (KTOR-8970)

3.3.1

Published 8 October 2025

Improvements

  • Add a note about SSE session lifetime in KDoc (KTOR-8440)
  • Update Kotlin to 2.2.20 (KTOR-8896)

... (truncated)

Commits
  • 629d22c Release 3.3.3 (#5219)
  • 77bbf10 Server Netty. Fix rejected execution during engine stop (#8671)
  • 75439b0 KTOR-9096 Fix SSE cancellation leak on Darwin (#5208)
  • 62ea8e4 KTOR-6837 Fix client URL handling for invalid DefaultRequest.host
  • ef09e84 Add stream.demand calls to JettyResponseListener
  • 47e6049 KTOR-9094 Jetty Client: Allow h2c traffic
  • e70922e KTOR-9125 Fix double ResponseSent invocation when exception is thrown after r...
  • 589f02d KTOR-3019 Improve CORS plugin logging to ease troubleshooting (#5194)
  • 5a66b47 Update Gradle and Develocity (#5201)
  • 2ba2f02 KTOR-8947 Filter pseudo-headers in Java client (#5198)
  • Additional commits viewable in compare view

Most Recent Ignore Conditions Applied to This Pull Request | Dependency Name | Ignore Conditions | | --- | --- | | io.ktor:ktor-bom | [>= 3.4.a, < 3.5] |
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index fa0cd8197..36f52442c 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin = "2.2.21" -ktor = "3.2.3" +ktor = "3.3.3" mcp-kotlin = "0.9.0" slf4j = "2.0.17" shadow = "9.4.1" From 6e247f902d77b0cf431cfac3bad3b6dede54749b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:32:24 +0300 Subject: [PATCH 058/187] chore(deps): bump mcp-kotlin from 0.9.0 to 0.10.0 in /samples/kotlin-mcp-server (#651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.9.0 to 0.10.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.9.0 to 0.10.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.10.0

Highlights

This release adds MCP conformance testing infrastructure, SSE reconnection support, Tasks protocol types, resource template matching, and
tool execution properties. It also completes a deprecation cycle — removing all previously deprecated (error-level) symbols from io.modelcontextprotocol.kotlin.sdk in favor of their replacements in io.modelcontextprotocol.kotlin.sdk.types.

Breaking Changes

  • Deprecation cycle completed — All symbols deprecated at ERROR level in 0.9.0 have been removed by @​devcrocod (#615). This includes
    the entire io.modelcontextprotocol.kotlin.sdk.types.kt compatibility file (type aliases such as CallToolResult, CancelledNotification, ClientCapabilities, CompleteRequest, etc.) and the legacy McpJson in io.modelcontextprotocol.kotlin.sdk. Migrate to the equivalents in io.modelcontextprotocol.kotlin.sdk.types.
  • SSE/Streamable HTTP transport changes — StreamableHttpClientTransport now accepts ReconnectionOptions for configurable retry behavior by @​devcrocod (#585, #596). The previous constructor signatures remain available.

Features

  • MCP conformance test infrastructure — Comprehensive conformance test suite covering core protocol operations, tool calls, elicitation, resources, prompts, and 20 OAuth/auth scenarios. Includes CI workflow, baseline tracking, and shell runner by @​devcrocod (#585)
  • SSE reconnection with retry support — Client transports now support configurable reconnection with exponential backoff via
    ReconnectionOptions (initial delay, max delay, multiplier, max retries) by @​devcrocod (#596)
  • Tasks protocol types — Added Task, TaskStatus, and related models per the MCP tasks specification by @​devcrocod (#566)
  • Resource templates with basic template matcher — Server-side support for URI template-based resource matching by @​kpavlov (#502, #606)
  • Tool execution properties — Tool now exposes execution properties for richer tool metadata by @​devcrocod (#567)

Bug Fixes

  • Notifications not delivered over Streamable HTTP — Fixed server-side notification delivery for Streamable HTTP transport sessions by @​kpavlov (#587, #599)
  • Conformance: SEP-1330 enum schemas — Corrected elicitation enum schemas in conformance tests by @​kpavlov (#600)

Documentation

Maintenance

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.9.0...0.10.0

Commits
  • 5ad0492 chore: bump project version to 0.10.0 in gradle.properties
  • bd4cf0c docs: add missing KDoc comments for public functions, properties, and classes...
  • c77ebe0 refactor(core): Fix Kotlin language and api version to 2.1 in build config (#...
  • cd91eed chore: simplify and clean up CI workflows (#617)
  • 435698a refactor(integration-test): modernize ts tests, update versions, rename SSE -...
  • 5583990 chore: deprecation cycle (#615)
  • 55afb63 chore(deps): bump gradle-wrapper from 9.3.1 to 9.4.1 (#614)
  • fd3a858 feat: add tasks models (#566)
  • 62e4e50 docs: update AGENTS.md (#612)
  • ee6561c feat: #502 resource templates with basic template matcher (#606)
  • Additional commits viewable in compare view

Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.9.0 to 0.10.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

0.10.0

Highlights

This release adds MCP conformance testing infrastructure, SSE reconnection support, Tasks protocol types, resource template matching, and
tool execution properties. It also completes a deprecation cycle — removing all previously deprecated (error-level) symbols from io.modelcontextprotocol.kotlin.sdk in favor of their replacements in io.modelcontextprotocol.kotlin.sdk.types.

Breaking Changes

  • Deprecation cycle completed — All symbols deprecated at ERROR level in 0.9.0 have been removed by @​devcrocod (#615). This includes
    the entire io.modelcontextprotocol.kotlin.sdk.types.kt compatibility file (type aliases such as CallToolResult, CancelledNotification, ClientCapabilities, CompleteRequest, etc.) and the legacy McpJson in io.modelcontextprotocol.kotlin.sdk. Migrate to the equivalents in io.modelcontextprotocol.kotlin.sdk.types.
  • SSE/Streamable HTTP transport changes — StreamableHttpClientTransport now accepts ReconnectionOptions for configurable retry behavior by @​devcrocod (#585, #596). The previous constructor signatures remain available.

Features

  • MCP conformance test infrastructure — Comprehensive conformance test suite covering core protocol operations, tool calls, elicitation, resources, prompts, and 20 OAuth/auth scenarios. Includes CI workflow, baseline tracking, and shell runner by @​devcrocod (#585)
  • SSE reconnection with retry support — Client transports now support configurable reconnection with exponential backoff via
    ReconnectionOptions (initial delay, max delay, multiplier, max retries) by @​devcrocod (#596)
  • Tasks protocol types — Added Task, TaskStatus, and related models per the MCP tasks specification by @​devcrocod (#566)
  • Resource templates with basic template matcher — Server-side support for URI template-based resource matching by @​kpavlov (#502, #606)
  • Tool execution properties — Tool now exposes execution properties for richer tool metadata by @​devcrocod (#567)

Bug Fixes

  • Notifications not delivered over Streamable HTTP — Fixed server-side notification delivery for Streamable HTTP transport sessions by @​kpavlov (#587, #599)
  • Conformance: SEP-1330 enum schemas — Corrected elicitation enum schemas in conformance tests by @​kpavlov (#600)

Documentation

Maintenance

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.9.0...0.10.0

Commits
  • 5ad0492 chore: bump project version to 0.10.0 in gradle.properties
  • bd4cf0c docs: add missing KDoc comments for public functions, properties, and classes...
  • c77ebe0 refactor(core): Fix Kotlin language and api version to 2.1 in build config (#...
  • cd91eed chore: simplify and clean up CI workflows (#617)
  • 435698a refactor(integration-test): modernize ts tests, update versions, rename SSE -...
  • 5583990 chore: deprecation cycle (#615)
  • 55afb63 chore(deps): bump gradle-wrapper from 9.3.1 to 9.4.1 (#614)
  • fd3a858 feat: add tasks models (#566)
  • 62e4e50 docs: update AGENTS.md (#612)
  • ee6561c feat: #502 resource templates with basic template matcher (#606)
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index 36f52442c..1c59a88b6 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.2.21" ktor = "3.3.3" -mcp-kotlin = "0.9.0" +mcp-kotlin = "0.10.0" slf4j = "2.0.17" shadow = "9.4.1" From ad072325c9f4e1feeb8bb2e532d9ec061c31959f Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:47:31 +0300 Subject: [PATCH 059/187] samples: update mcp-kotlin to 0.10.0 across sample projects (#645) ## Update mcp-kotlin to 0.10.0 across sample projects ## How Has This Been Tested? Locally, added ServerIntegrationTest to simple-streamable-server ## Breaking Changes No ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [x] Test update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- .../gradle/libs.versions.toml | 2 +- samples/notebooks/McpClient.ipynb | 517 ++++++++++-------- .../simple-streamable-server/build.gradle.kts | 9 + .../gradle/libs.versions.toml | 6 +- .../sample/server/ServerIntegrationTest.kt | 146 +++++ .../test/resources/simplelogger.properties | 6 + 6 files changed, 453 insertions(+), 233 deletions(-) create mode 100644 samples/simple-streamable-server/src/test/kotlin/io/modelcontextprotocol/sample/server/ServerIntegrationTest.kt create mode 100644 samples/simple-streamable-server/src/test/resources/simplelogger.properties diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 227422b8a..38cb588e3 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -2,7 +2,7 @@ anthropic = "2.18.0" kotlin = "2.2.21" ktor = "3.2.3" -mcp-kotlin = "0.9.0" +mcp-kotlin = "0.10.0" shadow = "9.4.1" slf4j = "2.0.17" diff --git a/samples/notebooks/McpClient.ipynb b/samples/notebooks/McpClient.ipynb index 583039d0d..54a095301 100644 --- a/samples/notebooks/McpClient.ipynb +++ b/samples/notebooks/McpClient.ipynb @@ -18,52 +18,65 @@ }, { "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_3_jupyter", + "Line_7_jupyter", + "Line_10_jupyter", + "Line_5_jupyter", + "Line_8_jupyter", + "Line_6_jupyter", + "Line_11_jupyter", + "Line_4_jupyter", + "Line_9_jupyter", + "Line_18_jupyter", + "Line_19_jupyter" + ] + }, "ExecuteTime": { - "end_time": "2025-12-04T16:08:57.281786Z", - "start_time": "2025-12-04T16:08:57.126434Z" + "end_time": "2026-03-30T10:42:19.114283Z", + "start_time": "2026-03-30T10:42:19.068020Z" } }, "cell_type": "code", "source": [ "%use coroutines (1.10.2)\n", - "%use ktor-client (0.3.0-5)\n", + "%use ktor-client (0.3.0-98)\n", "\n", "USE {\n", " dependencies(\n", - " \"io.modelcontextprotocol:kotlin-sdk-client-jvm:0.8.1\",\n", + " \"io.modelcontextprotocol:kotlin-sdk-client-jvm:0.10.0\",\n", " )\n", "}" ], "outputs": [], - "execution_count": 9 + "execution_count": 8 }, { "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_12_jupyter", + "Line_20_jupyter" + ] + }, "ExecuteTime": { - "end_time": "2025-12-04T16:08:57.298752Z", - "start_time": "2025-12-04T16:08:57.285099Z" + "end_time": "2026-03-30T10:42:19.131015Z", + "start_time": "2026-03-30T10:42:19.115475Z" } }, "cell_type": "code", "source": [ - "import kotlinx.coroutines.runBlocking\n", - "import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject\n", - "import io.modelcontextprotocol.kotlin.sdk.types.Implementation\n", - "import io.modelcontextprotocol.kotlin.sdk.types.TextContent\n", - "import io.modelcontextprotocol.kotlin.sdk.client.Client\n", - "import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions\n", - "import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport\n", - "import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport\n", "import io.ktor.client.HttpClient\n", - "import io.ktor.client.call.body\n", - "import io.ktor.client.request.get\n", - "import kotlinx.serialization.Serializable\n", - "import kotlinx.serialization.json.JsonObject\n", "import io.ktor.client.plugins.logging.*\n", - "import io.ktor.client.plugins.sse.SSE" + "import io.ktor.client.plugins.sse.SSE\n", + "import io.modelcontextprotocol.kotlin.sdk.client.Client\n", + "import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport\n", + "import io.modelcontextprotocol.kotlin.sdk.types.Implementation\n", + "import io.modelcontextprotocol.kotlin.sdk.types.TextContent" ], "outputs": [], - "execution_count": 10 + "execution_count": 9 }, { "metadata": {}, @@ -75,9 +88,15 @@ }, { "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_13_jupyter", + "Line_21_jupyter" + ] + }, "ExecuteTime": { - "end_time": "2025-12-04T16:08:57.346926Z", - "start_time": "2025-12-04T16:08:57.301809Z" + "end_time": "2026-03-30T10:42:19.189250Z", + "start_time": "2026-03-30T10:42:19.131969Z" } }, "cell_type": "code", @@ -88,14 +107,15 @@ " level = LogLevel.ALL\n", " logger = object : Logger {\n", " override fun log(message: String) {\n", - " DISPLAY(HTML(\"
HTTP Log
$message
\"))\n", + " val messageType = if (message.contains(\"REQUEST\")) \"Request\" else \"Response\"\n", + " DISPLAY(HTML(\"
HTTP $messageType
$message
\"))\n", " }\n", " }\n", " }\n", "}\n" ], "outputs": [], - "execution_count": 11 + "execution_count": 10 }, { "metadata": {}, @@ -104,9 +124,15 @@ }, { "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_14_jupyter", + "Line_22_jupyter" + ] + }, "ExecuteTime": { - "end_time": "2025-12-04T16:08:57.375128Z", - "start_time": "2025-12-04T16:08:57.350611Z" + "end_time": "2026-03-30T10:42:19.216018Z", + "start_time": "2026-03-30T10:42:19.190636Z" } }, "cell_type": "code", @@ -115,12 +141,11 @@ " clientInfo = Implementation(\n", " name = \"my-client\",\n", " version = \"1.0.0\"\n", - " ),\n", - " options = ClientOptions(),\n", + " )\n", ")" ], "outputs": [], - "execution_count": 12 + "execution_count": 11 }, { "metadata": {}, @@ -129,9 +154,15 @@ }, { "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_15_jupyter", + "Line_23_jupyter" + ] + }, "ExecuteTime": { - "end_time": "2025-12-04T16:08:58.199710Z", - "start_time": "2025-12-04T16:08:57.379661Z" + "end_time": "2026-03-30T10:42:19.748081Z", + "start_time": "2026-03-30T10:42:19.216824Z" } }, "cell_type": "code", @@ -152,7 +183,7 @@ { "data": { "text/html": [ - "
HTTP Log
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
        "METHOD: POST\n",
        "COMMON HEADERS\n",
        "-> Accept: application/json, text/event-stream\n",
@@ -162,7 +193,7 @@
        "-> Content-Type: application/json\n",
        "BODY Content-Type: application/json\n",
        "BODY START\n",
-       "{\"id\":\"26884bad1c9b4ea0b9c4470f893b60d3\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"my-client\",\"version\":\"1.0.0\"}},\"jsonrpc\":\"2.0\"}\n",
+       "{\"id\":\"8461d63b3d784df183832d109b688608\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-11-25\",\"capabilities\":{},\"clientInfo\":{\"name\":\"my-client\",\"version\":\"1.0.0\"}},\"jsonrpc\":\"2.0\"}\n",
        "BODY END
" ] }, @@ -175,31 +206,31 @@ { "data": { "text/html": [ - "
HTTP Log
RESPONSE: 200 OK\n",
+       "
HTTP Response
RESPONSE: 200 OK\n",
        "METHOD: POST\n",
        "FROM: https://learn.microsoft.com/api/mcp\n",
        "COMMON HEADERS\n",
-       "-> Akamai-Cache-Status: Miss from child, Miss from parent\n",
+       "-> Akamai-Cache-Status: Miss from child, NotCacheable from parent\n",
        "-> Cache-Control: no-cache, no-store\n",
        "-> Connection: keep-alive\n",
        "-> Content-Encoding: identity\n",
-       "-> Content-Length: 1947\n",
+       "-> Content-Length: 1983\n",
        "-> Content-Type: text/event-stream\n",
-       "-> Date: Thu, 04 Dec 2025 16:08:57 GMT\n",
-       "-> Expires: Thu, 04 Dec 2025 16:08:57 GMT\n",
-       "-> Mcp-Session-Id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
+       "-> Date: Mon, 30 Mar 2026 10:42:19 GMT\n",
+       "-> Expires: Mon, 30 Mar 2026 10:42:19 GMT\n",
        "-> Request-Context: appId=cid-v1:ccd19013-8f20-4877-8d3f-84a0107e7119\n",
        "-> Set-Cookie: ASLBSA=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; Path=/; Secure; HttpOnly;; ASLBSACORS=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; SameSite=none; Path=/; Secure; HttpOnly;\n",
        "-> Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",
        "-> X-Content-Type-Options: nosniff\n",
        "-> X-Powered-By: ASP.NET\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
        "-> nel: {\"report_to\":\"network-errors\",\"max_age\":604800,\"success_fraction\":0.01,\"failure_fraction\":1.0}\n",
        "-> report-to: {\"group\":\"network-errors\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://mdec.nelreports.net/api/report?cat=mdocs\"}]}\n",
-       "-> x-azure-ref: 20251204T160857Z-r1cfc47d796grb7bhC1STOs2rc0000002u20000000002xwb\n",
+       "-> x-azure-ref: 20260330T104219Z-157fb874dd7qc6ndhC1SVG8pe00000000740000000000xwt\n",
        "BODY Content-Type: text/event-stream\n",
        "BODY START\n",
        "event: message\n",
-       "data: {\"result\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{\"logging\":{},\"prompts\":{},\"resources\":{},\"tools\":{\"listChanged\":true}},\"serverInfo\":{\"name\":\"Microsoft Learn MCP Server\",\"version\":\"1.0.0\"},\"instructions\":\"# Microsoft Learn MCP Server\\r\\n\\r\\nThis server gives structured access to official Microsoft and Azure documentation via three tools:\\r\\n\\r\\n## Tools\\r\\n\\r\\n### microsoft_docs_search\\r\\nSearch official documentation and return up to 10 concise, high-quality content chunks (max 500 tokens each), including title, URL, and excerpt.\\r\\n\\r\\n- Use first to get a quick, reliable overview\\r\\n- Ideal for grounding answers in Microsoft knowledge\\r\\n\\r\\n### microsoft_code_sample_search\\r\\nSearch for code snippets and examples in official Microsoft Learn documentation and return up to 20 relevant, high-quality code samples.\\r\\n\\r\\n- Use when you need to provide sample Microsoft/Azure related code in your answers.  \\r\\n- Ideal for generating code snippets or practical implementation examples.  \\r\\n- Optional parameter \\u0060language\\u0060 can filter results. \\r\\n\\r\\n### microsoft_docs_fetch\\r\\nFetch and convert full Microsoft documentation pages to markdown.\\r\\n\\r\\n- Use after search when you need full content from a specific URL\\r\\n- Required for detailed tutorials, troubleshooting, prerequisites, code samples, or when search results are incomplete or outdated\\r\\n\\r\\n## Workflow\\r\\n\\r\\n1. Use \\u0060microsoft_docs_search\\u0060 to find relevant documents.\\r\\n2. If you need code examples or practical snippets, use \\u0060microsoft_code_sample_search\\u0060.\\r\\n3. If deeper or complete information is needed, use \\u0060microsoft_docs_fetch\\u0060.\\r\\n\\r\\n**Search gives breadth. Code Sample Search gives practical examples. Fetch gives depth.**\\r\\n\\r\\nAll content comes from Microsoft Learn or official sources, returned in clean markdown format.\"},\"id\":\"26884bad1c9b4ea0b9c4470f893b60d3\",\"jsonrpc\":\"2.0\"}\n",
+       "data: {\"result\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{\"logging\":{},\"prompts\":{\"listChanged\":true},\"resources\":{\"listChanged\":true},\"tools\":{\"listChanged\":true}},\"serverInfo\":{\"name\":\"Microsoft Learn MCP Server\",\"version\":\"1.0.0\"},\"instructions\":\"# Microsoft Learn MCP Server\\r\\n\\r\\nThis server gives structured access to official Microsoft and Azure documentation via three tools:\\r\\n\\r\\n## Tools\\r\\n\\r\\n### microsoft_docs_search\\r\\nSearch official documentation and return up to 10 concise, high-quality content chunks (max 500 tokens each), including title, URL, and excerpt.\\r\\n\\r\\n- Use first to get a quick, reliable overview\\r\\n- Ideal for grounding answers in Microsoft knowledge\\r\\n\\r\\n### microsoft_code_sample_search\\r\\nSearch for code snippets and examples in official Microsoft Learn documentation and return up to 20 relevant, high-quality code samples.\\r\\n\\r\\n- Use when you need to provide sample Microsoft/Azure related code in your answers.  \\r\\n- Ideal for generating code snippets or practical implementation examples.  \\r\\n- Optional parameter \\u0060language\\u0060 can filter results. \\r\\n\\r\\n### microsoft_docs_fetch\\r\\nFetch and convert full Microsoft documentation pages to markdown.\\r\\n\\r\\n- Use after search when you need full content from a specific URL\\r\\n- Required for detailed tutorials, troubleshooting, prerequisites, code samples, or when search results are incomplete or outdated\\r\\n\\r\\n## Workflow\\r\\n\\r\\n1. Use \\u0060microsoft_docs_search\\u0060 to find relevant documents.\\r\\n2. If you need code examples or practical snippets, use \\u0060microsoft_code_sample_search\\u0060.\\r\\n3. If deeper or complete information is needed, use \\u0060microsoft_docs_fetch\\u0060.\\r\\n\\r\\n**Search gives breadth. Code Sample Search gives practical examples. Fetch gives depth.**\\r\\n\\r\\nAll content comes from Microsoft Learn or official sources, returned in clean markdown format.\"},\"id\":\"8461d63b3d784df183832d109b688608\",\"jsonrpc\":\"2.0\"}\n",
        "\n",
        "\n",
        "BODY END
" @@ -214,12 +245,12 @@ { "data": { "text/html": [ - "
HTTP Log
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
        "METHOD: POST\n",
        "COMMON HEADERS\n",
        "-> Accept: application/json, text/event-stream\n",
        "-> Accept-Charset: UTF-8\n",
-       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
        "CONTENT HEADERS\n",
        "-> Content-Length: 54\n",
        "-> Content-Type: application/json\n",
@@ -238,26 +269,26 @@
     {
      "data": {
       "text/html": [
-       "
HTTP Log
RESPONSE: 202 Accepted\n",
+       "
HTTP Response
RESPONSE: 202 Accepted\n",
        "METHOD: POST\n",
        "FROM: https://learn.microsoft.com/api/mcp\n",
        "COMMON HEADERS\n",
-       "-> Akamai-Cache-Status: Miss from child, Miss from parent\n",
+       "-> Akamai-Cache-Status: Miss from child, NotCacheable from parent\n",
        "-> Cache-Control: no-cache, no-store\n",
        "-> Connection: keep-alive\n",
        "-> Content-Encoding: identity\n",
        "-> Content-Length: 0\n",
-       "-> Date: Thu, 04 Dec 2025 16:08:57 GMT\n",
-       "-> Expires: Thu, 04 Dec 2025 16:08:57 GMT\n",
-       "-> Mcp-Session-Id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
+       "-> Date: Mon, 30 Mar 2026 10:42:19 GMT\n",
+       "-> Expires: Mon, 30 Mar 2026 10:42:19 GMT\n",
        "-> Request-Context: appId=cid-v1:ccd19013-8f20-4877-8d3f-84a0107e7119\n",
        "-> Set-Cookie: ASLBSA=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; Path=/; Secure; HttpOnly;; ASLBSACORS=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; SameSite=none; Path=/; Secure; HttpOnly;\n",
        "-> Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",
        "-> X-Content-Type-Options: nosniff\n",
        "-> X-Powered-By: ASP.NET\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
        "-> nel: {\"report_to\":\"network-errors\",\"max_age\":604800,\"success_fraction\":0.01,\"failure_fraction\":1.0}\n",
        "-> report-to: {\"group\":\"network-errors\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://mdec.nelreports.net/api/report?cat=mdocs\"}]}\n",
-       "-> x-azure-ref: 20251204T160857Z-r1cfc47d796r8qqwhC1STO91g00000001p6g000000002rdg\n",
+       "-> x-azure-ref: 20260330T104219Z-157fb874dd7dr7drhC1SVGcxmc0000000pkg000000005v30\n",
        "BODY Content-Type: null\n",
        "BODY START\n",
        "\n",
@@ -270,15 +301,22 @@
       "display_id": null
      }
     },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "✅ Server version: Implementation(name=Microsoft Learn MCP Server, version=1.0.0, title=null, websiteUrl=null, icons=null)\n"
+     ]
+    },
     {
      "data": {
       "text/html": [
-       "
HTTP Log
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
        "METHOD: GET\n",
        "COMMON HEADERS\n",
        "-> Accept: application/json\n",
        "-> Accept-Charset: UTF-8\n",
-       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
        "CONTENT HEADERS\n",
        "-> Content-Length: 0\n",
        "-> Accept: text/event-stream\n",
@@ -299,47 +337,69 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "✅ Server version: Implementation(name=Microsoft Learn MCP Server, version=1.0.0, title=null, websiteUrl=null, icons=null)\n",
-      "✅ Server capabilities: ServerCapabilities(tools=Tools(listChanged=true), resources=Resources(listChanged=null, subscribe=null), prompts=Prompts(listChanged=null), logging={}, completions=null, experimental=null)\n"
+      "✅ Server capabilities: ServerCapabilities(tools=Tools(listChanged=true), resources=Resources(listChanged=true, subscribe=null), prompts=Prompts(listChanged=true), logging={}, completions=null, experimental=null)\n"
      ]
     }
    ],
-   "execution_count": 13
+   "execution_count": 12
   },
   {
    "metadata": {},
    "cell_type": "markdown",
-   "source": "Now we can send ping request to MCP server:"
+   "source": "Let's get a list of available tools:"
   },
   {
    "metadata": {
+    "executionRelatedData": {
+     "compiledClasses": [
+      "Line_16_jupyter",
+      "Line_24_jupyter"
+     ]
+    },
     "ExecuteTime": {
-     "end_time": "2025-12-04T16:08:58.507109Z",
-     "start_time": "2025-12-04T16:08:58.206412Z"
+     "end_time": "2026-03-30T10:42:20.410862Z",
+     "start_time": "2026-03-30T10:42:19.750112Z"
     }
    },
    "cell_type": "code",
    "source": [
     "runBlocking {\n",
-    "    mcpClient.ping()\n",
-    "}"
+    "    val listToolsResult = mcpClient.listTools()\n",
+    "\n",
+    "    for (tool in listToolsResult.tools) {\n",
+    "        println(\n",
+    "            \"\"\"\n",
+    "            |🔨Title: ${tool.title} (${tool.name})\n",
+    "            |\n",
+    "            |Description: ${tool.description}\n",
+    "            |\n",
+    "            |Input schema: ${tool.inputSchema}\n",
+    "            |Output schema: ${tool.outputSchema}\n",
+    "            |Icons: ${tool.icons?.joinToString(\", \")}\n",
+    "            |Annotations: ${tool.annotations}\n",
+    "            |Execution: ${tool.execution}\n",
+    "            |\n",
+    "            \"\"\".trimMargin()\n",
+    "        )\n",
+    "    }\n",
+    "}\n"
    ],
    "outputs": [
     {
      "data": {
       "text/html": [
-       "
HTTP Log
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
        "METHOD: POST\n",
        "COMMON HEADERS\n",
        "-> Accept: application/json, text/event-stream\n",
        "-> Accept-Charset: UTF-8\n",
-       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
        "CONTENT HEADERS\n",
-       "-> Content-Length: 73\n",
+       "-> Content-Length: 79\n",
        "-> Content-Type: application/json\n",
        "BODY Content-Type: application/json\n",
        "BODY START\n",
-       "{\"id\":\"caf63f9816a44766a926944329e987d5\",\"method\":\"ping\",\"jsonrpc\":\"2.0\"}\n",
+       "{\"id\":\"b9c6596da2514e04b08cbaa1defb300b\",\"method\":\"tools/list\",\"jsonrpc\":\"2.0\"}\n",
        "BODY END
" ] }, @@ -352,34 +412,7 @@ { "data": { "text/html": [ - "
HTTP Log
RESPONSE: 200 OK\n",
-       "METHOD: POST\n",
-       "FROM: https://learn.microsoft.com/api/mcp\n",
-       "COMMON HEADERS\n",
-       "-> Akamai-Cache-Status: Miss from child, Miss from parent\n",
-       "-> Cache-Control: no-cache, no-store\n",
-       "-> Connection: keep-alive\n",
-       "-> Content-Encoding: identity\n",
-       "-> Content-Length: 92\n",
-       "-> Content-Type: text/event-stream\n",
-       "-> Date: Thu, 04 Dec 2025 16:08:58 GMT\n",
-       "-> Expires: Thu, 04 Dec 2025 16:08:58 GMT\n",
-       "-> Mcp-Session-Id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
-       "-> Request-Context: appId=cid-v1:ccd19013-8f20-4877-8d3f-84a0107e7119\n",
-       "-> Set-Cookie: ASLBSA=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; Path=/; Secure; HttpOnly;; ASLBSACORS=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; SameSite=none; Path=/; Secure; HttpOnly;\n",
-       "-> Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",
-       "-> X-Content-Type-Options: nosniff\n",
-       "-> X-Powered-By: ASP.NET\n",
-       "-> nel: {\"report_to\":\"network-errors\",\"max_age\":604800,\"success_fraction\":0.01,\"failure_fraction\":1.0}\n",
-       "-> report-to: {\"group\":\"network-errors\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://mdec.nelreports.net/api/report?cat=mdocs\"}]}\n",
-       "-> x-azure-ref: 20251204T160858Z-r195dfd6579k9c28hC1STOn4n80000000nk0000000006rvx\n",
-       "BODY Content-Type: text/event-stream\n",
-       "BODY START\n",
-       "event: message\n",
-       "data: {\"result\":{},\"id\":\"caf63f9816a44766a926944329e987d5\",\"jsonrpc\":\"2.0\"}\n",
-       "\n",
-       "\n",
-       "BODY END
" + "
HTTP Response
RESPONSE https://learn.microsoft.com/api/mcp failed with exception: io.ktor.client.plugins.sse.SSEClientException: Expected status code 200 but was 405
" ] }, "metadata": {}, @@ -388,58 +421,26 @@ "display_id": null } }, - { - "data": { - "text/plain": [ - "EmptyResult(meta=null)" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "execution_count": 14 - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": "Let's get a list of available tools:" - }, - { - "metadata": { - "ExecuteTime": { - "end_time": "2025-12-04T16:08:58.818668Z", - "start_time": "2025-12-04T16:08:58.530569Z" - } - }, - "cell_type": "code", - "source": [ - "runBlocking {\n", - " val listToolsResult = mcpClient.listTools()\n", - "\n", - " for (tool in listToolsResult.tools) {\n", - " DISPLAY(tool)\n", - " }\n", - "}\n" - ], - "outputs": [ { "data": { "text/html": [ - "
HTTP Log
REQUEST: https://learn.microsoft.com/api/mcp\n",
-       "METHOD: POST\n",
+       "
HTTP Response
RESPONSE: 405 Method Not Allowed\n",
+       "METHOD: GET\n",
+       "FROM: https://learn.microsoft.com/api/mcp\n",
        "COMMON HEADERS\n",
-       "-> Accept: application/json, text/event-stream\n",
-       "-> Accept-Charset: UTF-8\n",
-       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
-       "CONTENT HEADERS\n",
-       "-> Content-Length: 79\n",
-       "-> Content-Type: application/json\n",
-       "BODY Content-Type: application/json\n",
-       "BODY START\n",
-       "{\"id\":\"1c46ee9521954378958971e267d543fc\",\"method\":\"tools/list\",\"jsonrpc\":\"2.0\"}\n",
-       "BODY END
" + "-> Akamai-Cache-Status: Miss from child, NotCacheable from parent\n", + "-> Cache-Control: max-age=0\n", + "-> Connection: keep-alive\n", + "-> Content-Length: 215\n", + "-> Date: Mon, 30 Mar 2026 10:42:19 GMT\n", + "-> Expires: Mon, 30 Mar 2026 10:42:19 GMT\n", + "-> Request-Context: appId=cid-v1:ccd19013-8f20-4877-8d3f-84a0107e7119\n", + "-> Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n", + "-> X-Content-Type-Options: nosniff\n", + "-> X-Powered-By: ASP.NET\n", + "-> nel: {\"report_to\":\"network-errors\",\"max_age\":604800,\"success_fraction\":0.01,\"failure_fraction\":1.0}\n", + "-> report-to: {\"group\":\"network-errors\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://mdec.nelreports.net/api/report?cat=mdocs\"}]}\n", + "-> x-azure-ref: 20260330T104219Z-157fb874dd7qc6ndhC1SVG8pe00000000740000000000xxc
" ] }, "metadata": {}, @@ -451,31 +452,31 @@ { "data": { "text/html": [ - "
HTTP Log
RESPONSE: 200 OK\n",
+       "
HTTP Response
RESPONSE: 200 OK\n",
        "METHOD: POST\n",
        "FROM: https://learn.microsoft.com/api/mcp\n",
        "COMMON HEADERS\n",
-       "-> Akamai-Cache-Status: Miss from child, Miss from parent\n",
+       "-> Akamai-Cache-Status: Miss from child, NotCacheable from parent\n",
        "-> Cache-Control: no-cache, no-store\n",
        "-> Connection: keep-alive\n",
        "-> Content-Encoding: identity\n",
-       "-> Content-Length: 4495\n",
+       "-> Content-Length: 4983\n",
        "-> Content-Type: text/event-stream\n",
-       "-> Date: Thu, 04 Dec 2025 16:08:58 GMT\n",
-       "-> Expires: Thu, 04 Dec 2025 16:08:58 GMT\n",
-       "-> Mcp-Session-Id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
+       "-> Date: Mon, 30 Mar 2026 10:42:20 GMT\n",
+       "-> Expires: Mon, 30 Mar 2026 10:42:20 GMT\n",
        "-> Request-Context: appId=cid-v1:ccd19013-8f20-4877-8d3f-84a0107e7119\n",
        "-> Set-Cookie: ASLBSA=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; Path=/; Secure; HttpOnly;; ASLBSACORS=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; SameSite=none; Path=/; Secure; HttpOnly;\n",
        "-> Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",
        "-> X-Content-Type-Options: nosniff\n",
        "-> X-Powered-By: ASP.NET\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
        "-> nel: {\"report_to\":\"network-errors\",\"max_age\":604800,\"success_fraction\":0.01,\"failure_fraction\":1.0}\n",
        "-> report-to: {\"group\":\"network-errors\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://mdec.nelreports.net/api/report?cat=mdocs\"}]}\n",
-       "-> x-azure-ref: 20251204T160858Z-r195dfd6579k9c28hC1STOn4n80000000nk0000000006rw8\n",
+       "-> x-azure-ref: 20260330T104219Z-157fb874dd7dr7drhC1SVGcxmc0000000pkg000000005v38\n",
        "BODY Content-Type: text/event-stream\n",
        "BODY START\n",
        "event: message\n",
-       "data: {\"result\":{\"tools\":[{\"name\":\"microsoft_docs_search\",\"title\":\"Microsoft Docs Search\",\"description\":\"Search official Microsoft/Azure documentation to find the most relevant and trustworthy content for a user\\u0027s query. This tool returns up to 10 high-quality content chunks (each max 500 tokens), extracted from Microsoft Learn and other official sources. Each result includes the article title, URL, and a self-contained content excerpt optimized for fast retrieval and reasoning. Always use this tool to quickly ground your answers in accurate, first-party Microsoft/Azure knowledge.\\n\\nThe \\u0060question\\u0060 parameter is no longer used, use \\u0060query\\u0060 instead.\\n\\n## Follow-up Pattern\\nTo ensure completeness, use microsoft_docs_fetch when high-value pages are identified by search. The fetch tool complements search by providing the full detail. This is a required step for comprehensive results.\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"description\":\"a query or topic about Microsoft/Azure products, services, platforms, developer tools, frameworks, or APIs\",\"type\":\"string\",\"default\":null},\"question\":{\"description\":\"this parameter is no longer used, use query instead.\",\"type\":\"string\",\"default\":null}}},\"annotations\":{\"title\":\"Microsoft Docs Search\",\"destructiveHint\":false,\"idempotentHint\":true,\"readOnlyHint\":true}},{\"name\":\"microsoft_code_sample_search\",\"title\":\"Microsoft Code Sample Search\",\"description\":\"Search for code snippets and examples in official Microsoft Learn documentation. This tool retrieves relevant code samples from Microsoft documentation pages providing developers with practical implementation examples and best practices for Microsoft/Azure products and services related coding tasks. This tool will help you use the **LATEST OFFICIAL** code snippets to empower coding capabilities.\\n\\n## When to Use This Tool\\n- When you are going to provide sample Microsoft/Azure related code snippets in your answers.\\n- When you are **generating any Microsoft/Azure related code**.\\n\\n## Usage Pattern\\nInput a descriptive query, or SDK/class/method name to retrieve related code samples. The optional parameter \\u0060language\\u0060 can help to filter results.\\n\\nEligible values for \\u0060language\\u0060 parameter include: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"description\":\"a descriptive query, SDK name, method name or code snippet related to Microsoft/Azure products, services, platforms, developer tools, frameworks, APIs or SDKs\",\"type\":\"string\"},\"language\":{\"description\":\"Optional parameter specifying the programming language of code snippets to retrieve. Can significantly improve search quality if provided. Eligible values: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php\",\"type\":\"string\",\"default\":null}},\"required\":[\"query\"]},\"annotations\":{\"title\":\"Microsoft Code Sample Search\",\"destructiveHint\":false,\"idempotentHint\":true,\"readOnlyHint\":true}},{\"name\":\"microsoft_docs_fetch\",\"title\":\"Microsoft Docs Fetch\",\"description\":\"Fetch and convert a Microsoft Learn documentation page to markdown format. This tool retrieves the latest complete content of Microsoft documentation pages including Azure, .NET, Microsoft 365, and other Microsoft technologies.\\n\\n## When to Use This Tool\\n- When search results provide incomplete information or truncated content\\n- When you need complete step-by-step procedures or tutorials\\n- When you need troubleshooting sections, prerequisites, or detailed explanations\\n- When search results reference a specific page that seems highly relevant\\n- For comprehensive guides that require full context\\n\\n## Usage Pattern\\nUse this tool AFTER microsoft_docs_search when you identify specific high-value pages that need complete content. The search tool gives you an overview; this tool gives you the complete picture.\\n\\n## URL Requirements\\n- The URL must be a valid link from the microsoft.com domain.\\n\\n## Output Format\\nmarkdown with headings, code blocks, tables, and links preserved.\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"url\":{\"description\":\"URL of the Microsoft documentation page to read\",\"type\":\"string\"}},\"required\":[\"url\"]},\"annotations\":{\"title\":\"Microsoft Docs Fetch\",\"destructiveHint\":false,\"idempotentHint\":true,\"readOnlyHint\":true}}]},\"id\":\"1c46ee9521954378958971e267d543fc\",\"jsonrpc\":\"2.0\"}\n",
+       "data: {\"result\":{\"tools\":[{\"name\":\"microsoft_docs_search\",\"title\":\"Microsoft Docs Search\",\"description\":\"Search official Microsoft/Azure documentation to find the most relevant and trustworthy content for a user\\u0027s query. This tool returns up to 10 high-quality content chunks (each max 500 tokens), extracted from Microsoft Learn and other official sources. Each result includes the article title, URL, and a self-contained content excerpt optimized for fast retrieval and reasoning. Always use this tool to quickly ground your answers in accurate, first-party Microsoft/Azure knowledge.\\n\\n## Follow-up Pattern\\nTo ensure completeness, use microsoft_docs_fetch when high-value pages are identified by search. The fetch tool complements search by providing the full detail. This is a required step for comprehensive results.\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"description\":\"a query or topic about Microsoft/Azure products, services, platforms, developer tools, frameworks, or APIs\",\"type\":\"string\",\"default\":null}}},\"outputSchema\":{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"title\":{\"type\":\"string\"},\"content\":{\"type\":[\"string\",\"null\"]},\"contentUrl\":{\"type\":\"string\"},\"content_omitted\":{\"type\":\"boolean\"},\"extensionData\":{\"type\":[\"object\",\"null\"]}}}}}},\"annotations\":{\"title\":\"Microsoft Docs Search\",\"destructiveHint\":false,\"idempotentHint\":true,\"readOnlyHint\":true}},{\"name\":\"microsoft_code_sample_search\",\"title\":\"Microsoft Code Sample Search\",\"description\":\"Search for code snippets and examples in official Microsoft Learn documentation. This tool retrieves relevant code samples from Microsoft documentation pages providing developers with practical implementation examples and best practices for Microsoft/Azure products and services related coding tasks. This tool will help you use the **LATEST OFFICIAL** code snippets to empower coding capabilities.\\n\\n## When to Use This Tool\\n- When you are going to provide sample Microsoft/Azure related code snippets in your answers.\\n- When you are **generating any Microsoft/Azure related code**.\\n\\n## Usage Pattern\\nInput a descriptive query, or SDK/class/method name to retrieve related code samples. The optional parameter \\u0060language\\u0060 can help to filter results.\\n\\nEligible values for \\u0060language\\u0060 parameter include: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"description\":\"a descriptive query, SDK name, method name or code snippet related to Microsoft/Azure products, services, platforms, developer tools, frameworks, APIs or SDKs\",\"type\":\"string\"},\"language\":{\"description\":\"Optional parameter specifying the programming language of code snippets to retrieve. Can significantly improve search quality if provided. Eligible values: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php\",\"type\":\"string\",\"default\":null}},\"required\":[\"query\"]},\"outputSchema\":{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\"},\"codeSnippet\":{\"type\":[\"string\",\"null\"]},\"link\":{\"type\":\"string\"},\"language\":{\"type\":\"string\"},\"content_omitted\":{\"type\":\"boolean\"}}}}}},\"annotations\":{\"title\":\"Microsoft Code Sample Search\",\"destructiveHint\":false,\"idempotentHint\":true,\"readOnlyHint\":true}},{\"name\":\"microsoft_docs_fetch\",\"title\":\"Microsoft Docs Fetch\",\"description\":\"Fetch and convert a Microsoft Learn documentation webpage to markdown format. This tool retrieves the latest complete content of Microsoft documentation webpages including Azure, .NET, Microsoft 365, and other Microsoft technologies.\\n\\n## When to Use This Tool\\n- When search results provide incomplete information or truncated content\\n- When you need complete step-by-step procedures or tutorials\\n- When you need troubleshooting sections, prerequisites, or detailed explanations\\n- When search results reference a specific page that seems highly relevant\\n- For comprehensive guides that require full context\\n\\n## Usage Pattern\\nUse this tool AFTER microsoft_docs_search when you identify specific high-value pages that need complete content. The search tool gives you an overview; this tool gives you the complete picture.\\n\\n## URL Requirements\\n- The URL must be a valid HTML documentation webpage from the microsoft.com domain\\n- Binary files (PDF, DOCX, images, etc.) are not supported\\n\\n## Output Format\\nmarkdown with headings, code blocks, tables, and links preserved.\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"url\":{\"description\":\"URL of the Microsoft documentation page to read\",\"type\":\"string\"}},\"required\":[\"url\"]},\"annotations\":{\"title\":\"Microsoft Docs Fetch\",\"destructiveHint\":false,\"idempotentHint\":true,\"readOnlyHint\":true}}]},\"id\":\"b9c6596da2514e04b08cbaa1defb300b\",\"jsonrpc\":\"2.0\"}\n",
        "\n",
        "\n",
        "BODY END
" @@ -488,73 +489,72 @@ } }, { - "data": { - "text/plain": [ - "Tool(name=microsoft_docs_search, inputSchema=ToolSchema(properties={\"query\":{\"description\":\"a query or topic about Microsoft/Azure products, services, platforms, developer tools, frameworks, or APIs\",\"type\":\"string\",\"default\":null},\"question\":{\"description\":\"this parameter is no longer used, use query instead.\",\"type\":\"string\",\"default\":null}}, required=null), description=Search official Microsoft/Azure documentation to find the most relevant and trustworthy content for a user's query. This tool returns up to 10 high-quality content chunks (each max 500 tokens), extracted from Microsoft Learn and other official sources. Each result includes the article title, URL, and a self-contained content excerpt optimized for fast retrieval and reasoning. Always use this tool to quickly ground your answers in accurate, first-party Microsoft/Azure knowledge.\n", - "\n", - "The `question` parameter is no longer used, use `query` instead.\n", - "\n", - "## Follow-up Pattern\n", - "To ensure completeness, use microsoft_docs_fetch when high-value pages are identified by search. The fetch tool complements search by providing the full detail. This is a required step for comprehensive results., outputSchema=null, title=Microsoft Docs Search, annotations=ToolAnnotations(title=Microsoft Docs Search, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null), icons=null, meta=null)" - ] - }, - "metadata": {}, - "output_type": "display_data", - "jetTransient": { - "display_id": null - } - }, - { - "data": { - "text/plain": [ - "Tool(name=microsoft_code_sample_search, inputSchema=ToolSchema(properties={\"query\":{\"description\":\"a descriptive query, SDK name, method name or code snippet related to Microsoft/Azure products, services, platforms, developer tools, frameworks, APIs or SDKs\",\"type\":\"string\"},\"language\":{\"description\":\"Optional parameter specifying the programming language of code snippets to retrieve. Can significantly improve search quality if provided. Eligible values: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php\",\"type\":\"string\",\"default\":null}}, required=[query]), description=Search for code snippets and examples in official Microsoft Learn documentation. This tool retrieves relevant code samples from Microsoft documentation pages providing developers with practical implementation examples and best practices for Microsoft/Azure products and services related coding tasks. This tool will help you use the **LATEST OFFICIAL** code snippets to empower coding capabilities.\n", - "\n", - "## When to Use This Tool\n", - "- When you are going to provide sample Microsoft/Azure related code snippets in your answers.\n", - "- When you are **generating any Microsoft/Azure related code**.\n", - "\n", - "## Usage Pattern\n", - "Input a descriptive query, or SDK/class/method name to retrieve related code samples. The optional parameter `language` can help to filter results.\n", - "\n", - "Eligible values for `language` parameter include: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php, outputSchema=null, title=Microsoft Code Sample Search, annotations=ToolAnnotations(title=Microsoft Code Sample Search, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null), icons=null, meta=null)" - ] - }, - "metadata": {}, - "output_type": "display_data", - "jetTransient": { - "display_id": null - } - }, - { - "data": { - "text/plain": [ - "Tool(name=microsoft_docs_fetch, inputSchema=ToolSchema(properties={\"url\":{\"description\":\"URL of the Microsoft documentation page to read\",\"type\":\"string\"}}, required=[url]), description=Fetch and convert a Microsoft Learn documentation page to markdown format. This tool retrieves the latest complete content of Microsoft documentation pages including Azure, .NET, Microsoft 365, and other Microsoft technologies.\n", - "\n", - "## When to Use This Tool\n", - "- When search results provide incomplete information or truncated content\n", - "- When you need complete step-by-step procedures or tutorials\n", - "- When you need troubleshooting sections, prerequisites, or detailed explanations\n", - "- When search results reference a specific page that seems highly relevant\n", - "- For comprehensive guides that require full context\n", - "\n", - "## Usage Pattern\n", - "Use this tool AFTER microsoft_docs_search when you identify specific high-value pages that need complete content. The search tool gives you an overview; this tool gives you the complete picture.\n", - "\n", - "## URL Requirements\n", - "- The URL must be a valid link from the microsoft.com domain.\n", - "\n", - "## Output Format\n", - "markdown with headings, code blocks, tables, and links preserved., outputSchema=null, title=Microsoft Docs Fetch, annotations=ToolAnnotations(title=Microsoft Docs Fetch, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null), icons=null, meta=null)" - ] - }, - "metadata": {}, - "output_type": "display_data", - "jetTransient": { - "display_id": null - } + "name": "stdout", + "output_type": "stream", + "text": [ + "🔨Title: Microsoft Docs Search (microsoft_docs_search)\n", + "\n", + "Description: Search official Microsoft/Azure documentation to find the most relevant and trustworthy content for a user's query. This tool returns up to 10 high-quality content chunks (each max 500 tokens), extracted from Microsoft Learn and other official sources. Each result includes the article title, URL, and a self-contained content excerpt optimized for fast retrieval and reasoning. Always use this tool to quickly ground your answers in accurate, first-party Microsoft/Azure knowledge.\n", + "\n", + "## Follow-up Pattern\n", + "To ensure completeness, use microsoft_docs_fetch when high-value pages are identified by search. The fetch tool complements search by providing the full detail. This is a required step for comprehensive results.\n", + "\n", + "Input schema: ToolSchema(properties={\"query\":{\"description\":\"a query or topic about Microsoft/Azure products, services, platforms, developer tools, frameworks, or APIs\",\"type\":\"string\",\"default\":null}}, required=null, defs=null)\n", + "Output schema: ToolSchema(properties={\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"title\":{\"type\":\"string\"},\"content\":{\"type\":[\"string\",\"null\"]},\"contentUrl\":{\"type\":\"string\"},\"content_omitted\":{\"type\":\"boolean\"},\"extensionData\":{\"type\":[\"object\",\"null\"]}}}}}, required=null, defs=null)\n", + "Icons: null\n", + "Annotations: ToolAnnotations(title=Microsoft Docs Search, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null)\n", + "Execution: null\n", + "\n", + "🔨Title: Microsoft Code Sample Search (microsoft_code_sample_search)\n", + "\n", + "Description: Search for code snippets and examples in official Microsoft Learn documentation. This tool retrieves relevant code samples from Microsoft documentation pages providing developers with practical implementation examples and best practices for Microsoft/Azure products and services related coding tasks. This tool will help you use the **LATEST OFFICIAL** code snippets to empower coding capabilities.\n", + "\n", + "## When to Use This Tool\n", + "- When you are going to provide sample Microsoft/Azure related code snippets in your answers.\n", + "- When you are **generating any Microsoft/Azure related code**.\n", + "\n", + "## Usage Pattern\n", + "Input a descriptive query, or SDK/class/method name to retrieve related code samples. The optional parameter `language` can help to filter results.\n", + "\n", + "Eligible values for `language` parameter include: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php\n", + "\n", + "Input schema: ToolSchema(properties={\"query\":{\"description\":\"a descriptive query, SDK name, method name or code snippet related to Microsoft/Azure products, services, platforms, developer tools, frameworks, APIs or SDKs\",\"type\":\"string\"},\"language\":{\"description\":\"Optional parameter specifying the programming language of code snippets to retrieve. Can significantly improve search quality if provided. Eligible values: csharp javascript typescript python powershell azurecli al sql java kusto cpp go rust ruby php\",\"type\":\"string\",\"default\":null}}, required=[query], defs=null)\n", + "Output schema: ToolSchema(properties={\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\"},\"codeSnippet\":{\"type\":[\"string\",\"null\"]},\"link\":{\"type\":\"string\"},\"language\":{\"type\":\"string\"},\"content_omitted\":{\"type\":\"boolean\"}}}}}, required=null, defs=null)\n", + "Icons: null\n", + "Annotations: ToolAnnotations(title=Microsoft Code Sample Search, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null)\n", + "Execution: null\n", + "\n", + "🔨Title: Microsoft Docs Fetch (microsoft_docs_fetch)\n", + "\n", + "Description: Fetch and convert a Microsoft Learn documentation webpage to markdown format. This tool retrieves the latest complete content of Microsoft documentation webpages including Azure, .NET, Microsoft 365, and other Microsoft technologies.\n", + "\n", + "## When to Use This Tool\n", + "- When search results provide incomplete information or truncated content\n", + "- When you need complete step-by-step procedures or tutorials\n", + "- When you need troubleshooting sections, prerequisites, or detailed explanations\n", + "- When search results reference a specific page that seems highly relevant\n", + "- For comprehensive guides that require full context\n", + "\n", + "## Usage Pattern\n", + "Use this tool AFTER microsoft_docs_search when you identify specific high-value pages that need complete content. The search tool gives you an overview; this tool gives you the complete picture.\n", + "\n", + "## URL Requirements\n", + "- The URL must be a valid HTML documentation webpage from the microsoft.com domain\n", + "- Binary files (PDF, DOCX, images, etc.) are not supported\n", + "\n", + "## Output Format\n", + "markdown with headings, code blocks, tables, and links preserved.\n", + "\n", + "Input schema: ToolSchema(properties={\"url\":{\"description\":\"URL of the Microsoft documentation page to read\",\"type\":\"string\"}}, required=[url], defs=null)\n", + "Output schema: null\n", + "Icons: null\n", + "Annotations: ToolAnnotations(title=Microsoft Docs Fetch, readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=null)\n", + "Execution: null\n", + "\n" + ] } ], - "execution_count": 15 + "execution_count": 13 }, { "metadata": {}, @@ -563,9 +563,15 @@ }, { "metadata": { + "executionRelatedData": { + "compiledClasses": [ + "Line_17_jupyter", + "Line_25_jupyter" + ] + }, "ExecuteTime": { - "end_time": "2025-12-04T16:08:59.912312Z", - "start_time": "2025-12-04T16:08:58.839853Z" + "end_time": "2026-03-30T10:42:21.227094Z", + "start_time": "2026-03-30T10:42:20.419561Z" } }, "cell_type": "code", @@ -575,9 +581,19 @@ " name = \"microsoft_docs_search\",\n", " arguments = mapOf(\"query\" to \"Linux\")\n", " )\n", - " toolCallResult?.let {\n", - " for (content in it.content) {\n", - " println(content)\n", + " for (content in toolCallResult.content) {\n", + " (content as? TextContent).let {\n", + " Json.parseToJsonElement(it?.text.orEmpty()).jsonObject.get(\"results\")?.jsonArray?.forEach { result ->\n", + " val item = result as JsonObject\n", + " println(\n", + " \"\"\"\n", + " |Title: ${item[\"title\"]}\n", + " |\n", + " |Content: ${item[\"content\"]}\n", + " |Content URL: ${item[\"contentUrl\"]}\n", + " \"\"\".trimMargin()\n", + " )\n", + " }\n", " }\n", " }\n", "}\n" @@ -586,18 +602,18 @@ { "data": { "text/html": [ - "
HTTP Log
REQUEST: https://learn.microsoft.com/api/mcp\n",
+       "
HTTP Request
REQUEST: https://learn.microsoft.com/api/mcp\n",
        "METHOD: POST\n",
        "COMMON HEADERS\n",
        "-> Accept: application/json, text/event-stream\n",
        "-> Accept-Charset: UTF-8\n",
-       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
        "CONTENT HEADERS\n",
        "-> Content-Length: 162\n",
        "-> Content-Type: application/json\n",
        "BODY Content-Type: application/json\n",
        "BODY START\n",
-       "{\"id\":\"c4d77709f5314c9e9f097f08dd756438\",\"method\":\"tools/call\",\"params\":{\"name\":\"microsoft_docs_search\",\"arguments\":{\"query\":\"Linux\"},\"_meta\":{}},\"jsonrpc\":\"2.0\"}\n",
+       "{\"id\":\"07e26d19ee6248d98f6dc039bf4fc589\",\"method\":\"tools/call\",\"params\":{\"name\":\"microsoft_docs_search\",\"arguments\":{\"query\":\"Linux\"},\"_meta\":{}},\"jsonrpc\":\"2.0\"}\n",
        "BODY END
" ] }, @@ -610,31 +626,31 @@ { "data": { "text/html": [ - "
HTTP Log
RESPONSE: 200 OK\n",
+       "
HTTP Response
RESPONSE: 200 OK\n",
        "METHOD: POST\n",
        "FROM: https://learn.microsoft.com/api/mcp\n",
        "COMMON HEADERS\n",
-       "-> Akamai-Cache-Status: Miss from child, Miss from parent\n",
+       "-> Akamai-Cache-Status: Miss from child, NotCacheable from parent\n",
        "-> Cache-Control: no-cache, no-store\n",
        "-> Connection: keep-alive\n",
        "-> Content-Encoding: identity\n",
-       "-> Content-Length: 24086\n",
+       "-> Content-Length: 49801\n",
        "-> Content-Type: text/event-stream\n",
-       "-> Date: Thu, 04 Dec 2025 16:08:59 GMT\n",
-       "-> Expires: Thu, 04 Dec 2025 16:08:59 GMT\n",
-       "-> Mcp-Session-Id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIn0sInVzZXJJZENsYWltIjpudWxsfQ\n",
+       "-> Date: Mon, 30 Mar 2026 10:42:21 GMT\n",
+       "-> Expires: Mon, 30 Mar 2026 10:42:21 GMT\n",
        "-> Request-Context: appId=cid-v1:ccd19013-8f20-4877-8d3f-84a0107e7119\n",
        "-> Set-Cookie: ASLBSA=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; Path=/; Secure; HttpOnly;; ASLBSACORS=0003562b174eb06eed9ae0ec705e26d2c9aec6332388d120314730a5c2da183d42c8; SameSite=none; Path=/; Secure; HttpOnly;\n",
        "-> Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",
        "-> X-Content-Type-Options: nosniff\n",
        "-> X-Powered-By: ASP.NET\n",
+       "-> mcp-session-id: eyJjbGllbnRJbmZvIjp7Im5hbWUiOiJteS1jbGllbnQiLCJ0aXRsZSI6bnVsbCwidmVyc2lvbiI6IjEuMC4wIiwiaWNvbnMiOm51bGwsIndlYnNpdGVVcmwiOm51bGx9LCJ1c2VySWRDbGFpbSI6IjQ4NjFlZWMxLWRlNGUtNGZhMi1hMjA4LWJkNzNhNDdkYmE3OCJ9\n",
        "-> nel: {\"report_to\":\"network-errors\",\"max_age\":604800,\"success_fraction\":0.01,\"failure_fraction\":1.0}\n",
        "-> report-to: {\"group\":\"network-errors\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://mdec.nelreports.net/api/report?cat=mdocs\"}]}\n",
-       "-> x-azure-ref: 20251204T160859Z-r195dfd6579k9c28hC1STOn4n80000000nk0000000006rws\n",
+       "-> x-azure-ref: 20260330T104220Z-157fb874dd7dr7drhC1SVGcxmc0000000pkg000000005v3x\n",
        "BODY Content-Type: text/event-stream\n",
        "BODY START\n",
        "event: message\n",
-       "data: {\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"[{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 2 - Choose a Linux distribution\\\\nThe different versions of Linux are called \\\\u0022distributions\\\\u0022 (sometimes shortened to \\\\u201Cdistros\\\\u201D). So there isn\\\\u2019t actually a way to just \\\\u201Cinstall Linux\\\\u201D, you first need to choose which distribution that uses the Linux kernel you want to download and install. There are over 600 active Linux distributions, such as Ubuntu, Debian, Kali Linux, openSUSE, and more. There are a number of factors to consider in choosing a distribution, such as:\\\\n### Required Linux experience\\\\nIf you\\\\u2019re new to Linux, you may want to start with Ubuntu. This is one of the most common starting places for Linux users due to the level of support and ease of use. Ubuntu offers a desktop-version and a server-version depending on how you plan to use it. The specific tasks that the operating system will be responsible for may help you determine which distribution to use. Arch Linux is a popular choice for those who want a highly customizable, do-it-yourself approach that is still stable and well-supported by a large user base. It is a much more complicated place to start, but can help you to get a better understanding of how Linux works due to the amount of custom configuration.\\\\n### System requirements\\\\nA minimal, or \\\\u0022lightweight\\\\u0022, distribution means a small footprint, with lower memory and processing speed requirements. Alpine Linux is a popular option where users will find most features disabled or not installed by default, but still has everything you need for a containerized app. Security needs: Some Linux users may have specific security concerns to consider. Kali Linux is an example of a distro designed for security tasks, such as Penetration Testing, Security Research, Computer Forensics and Reverse Engineering.\\\\n### Business and Enterprise requirements\\\\nBusiness, corporate, or academic environments may have unique concerns from consumer environments. For example Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES) and Oracle Linux offer 24/7 global support and subscription-based service. CentOS is another popular enterprise distro that is community-supported but still compatible with RHEL.\\\\n### Community support\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/linux/install#step-2---choose-a-linux-distribution\\u0022},{\\u0022title\\u0022:\\u0022New to Linux resources for SQL users\\u0022,\\u0022content\\u0022:\\u0022# New to Linux resources for SQL users\\\\n**Applies to:**[SQL Server](https://learn.microsoft.com/en-us/sql/sql-server/sql-docs-navigation-guide?view=sql-server-ver17#applies-to) - Linux\\\\nThis article is intended to provide a learning path through an introduction of Linux concepts. Use the sections in the article as a guided learning path to familiarize yourself with the Linux environment.\\\\nThis isn\\\\u0027t intended to be a comprehensive list but instead provides the bare minimum you need to help you administer and navigate your SQL Server on Linux environment. See the [full list of tutorials](https://www.linux.org/forums/linux-beginner-tutorials.123/) to learn more.\\\\n## What is Linux?\\\\nThe [What is Linux](https://www.linux.org/threads/what-is-linux.4106/) module introduces the history of Linux as an operating system. The module explains *the Kernel* and where Linux is today. This tutorial serves as an introduction to Linux, helping you get started.\\\\n## Select a distribution\\\\nAfter you know the history of Linux, decide which [distribution of Linux](https://www.linux.org/threads/selecting-a-linux-distribution.4117/) best suits your business needs. [SQL Server is supported](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-release-notes-2019?view=sql-server-ver17#supported-platforms) on various distributions such as Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES), and Ubuntu.\\\\n## Get around directories\\\\nAfter you have chosen the appropriate distribution of Linux, familiarize yourself with [getting around Linux directories](https://www.linux.org/threads/getting-around-in-linux-directories.4120/).\\\\nThis module will help you answer the following questions:\\\\n1. How to navigate between different files\\\\n2. How to identify a file from a directory\\\\n3. Identify different directories\\\\n## Install new software\\\\nNext, learn how to [install new software](https://www.linux.org/threads/installing-new-software-debian-red-hat-slackware.4119/) on your new Linux operating system. This module details installing new software to the Debian and Red Hat Linux operating systems.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/sql/linux/new-to-linux-resources?view=sql-server-ver17\\u0022},{\\u0022title\\u0022:\\u0022Part 1.2 - Basic operations on Linux\\u0022,\\u0022content\\u0022:\\u0022# Part 1.2 - Basic operations on Linux\\\\n## Run commands by using the \\\\u0022sudo\\\\u0022 prefix\\\\nInstead of becoming the root account user in your session, you can run commands as root by using the \\\\u0060sudo \\\\u003Ccommand\\\\u003E\\\\u0060 format. This approach is safer because it will run the given command only as a superuser.\\\\nFor example, if you run \\\\u0060restart\\\\u0060 while you\\\\u0027re not elevated to superuser status, the result is as follows.\\\\n![Screenshot of restart command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/restart-command.png)\\\\nTo run this command as a superuser instead, you can either change the session context to root (by running \\\\u0060sudo su\\\\u0060) or add the \\\\u0060sudo\\\\u0060 prefix, as follows:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\nsudo reboot\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nNow, the restart operation runs as expected by the virtual machine.\\\\n![Screenshot of reboot command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/reboot-command.png)\\\\n## Package managers\\\\nPackage managers are used to install, upgrade, and remove applications in Linux. There are several different package managers, such as DPKG (Debian Package Management System), RPM (Red Hat Package Manager), and Zypper for SUSE Package managers themselves are low-level. Therefore, it might be difficult to manage software directly by using the package manager. Fortunately, there are wrappers for these tools that make it easier to install and uninstall software. For example, [yum](https://wikipedia.org/wiki/Yum_%28software%29) is a wrapper around RPM (Red Hat Package Manager) and [APT](https://wikipedia.org/wiki/APT_%28software%29) is a wrapper around DPKG (Debian Package Management System).\\\\nThis tutorial assumes that you\\\\u0027re running the [Ubuntu package management system](https://ubuntu.com/server/docs/package-management). This system is derived from the same system that\\\\u0027s used by the Debian GNU/Linux distribution. Therefore, we can use APT to install the software.\\\\nDepending on your Linux choice, you might have to use other tools. For example, you can use yum to install or uninstall software if you\\\\u0027re running Red Hat.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-by-using-the-sudo-prefix\\u0022},{\\u0022title\\u0022:\\u0022Part 1.2 - Basic operations on Linux\\u0022,\\u0022content\\u0022:\\u0022# Part 1.2 - Basic operations on Linux\\\\n## Run commands as a superuser\\\\nThe superuser is the most privileged user account. It has root (unrestricted) access to all files and folders, and complete control over the operation of the computer. The system administrator uses this account for system maintenance.\\\\nOn UNIX-like systems, the conventional name of the superuser is \\\\u0022root\\\\u0022. The root user can do tasks that are otherwise restricted for standard users, including the following ones:\\\\n1. Changing system directories\\\\n2. Changing user privileges\\\\n3. Changing file ownership\\\\n4. System-related tasks such as restarts\\\\n5. Installation of certain applications\\\\nThere are several ways to run commands as the root account user. One is to use the \\\\u0060sudo su\\\\u0060 command. In this command, \\\\u0060sudo\\\\u0060 is short for \\\\u0060superuser do\\\\u0060, and \\\\u0060su\\\\u0060 is an abbreviation for \\\\u0022switch user.\\\\u0022 You can also use the \\\\u0060su\\\\u0060 command to switch between standard users.\\\\nImportant\\\\nAfter you become the root account user, everything you do runs in root context. Therefore, you should act carefully because the root account has access to the entire system.\\\\nTo demonstrate a permissions issue, create a text file in a special folder that\\\\u0027s named */etc*.\\\\nThe */etc/* folder is where system configuration files are located. This folder protects its contents from being changed by a standard user.\\\\nRun the following command:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\necho hello world \\\\u003E /etc/helloworld.txt\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nThe \\\\u0060echo\\\\u0060 command writes all the text that follows it to the output. The angle bracket (\\\\u0060\\\\u003E\\\\u0060) tells the system to send the output to the */etc/helloworld.txt* file instead of the console. This behavior is similar to how Windows works.\\\\nNote\\\\nLinux is case-sensitive: *Helloworld.txt*, *helloworld.txt* and *helloworld.Txt* are different files.\\\\nYou don\\\\u0027t have to provide a file name extension. The name \\\\u0022helloworld\\\\u0022 by itself is perfectly valid. (The \\\\u0022txt\\\\u0022 extension is used here only as an example.)\\\\nWhen you run this command, you receive a **permission denied** error message.\\\\n![Screenshot of permission denied message.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/permission-denied-message.png)\\\\nThe operation fails because the */etc* folder is special folder that a standard user can\\\\u0027t change. Verify the user by running the \\\\u0060id\\\\u0060 command.\\\\n![Screenshot of I D command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/id-command.png)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 1 - Choose a method to install Linux\\\\nThe choice of which method to use for installing Linux depends on your needs and preferences.\\\\n1. **New to using Linux?** We recommend starting with Windows Subsystem for Linux (WSL) as it\\\\u0027s the easiest way to get up and running.\\\\n2. **Work in a business environment with more complex needs related to scale or security?** We recommend running Linux as a Virtual Machine (VM) in the cloud and checking out the support that Azure has to offer. This also applies if you want to run Linux as a server.\\\\n3. **Only want to run Linux as your primary operating system?** If you are good with a slightly more complicated install process and don\\\\u0027t need access to Windows tools (like Outlook, Teams, Word, PowerPoint, etc), you can run Linux on bare metal to access the full potential of your hardware without any overhead from virtualization or emulation.\\\\nLearn more about these options below.\\\\n### Windows Subsystem for Linux (WSL)\\\\nAvailable to those with a Windows machine, this is the most simple way to install Linux. Just run the Linux install command: \\\\u0060wsl --install\\\\u0060 to install the Ubuntu distribution.\\\\nThis method uses virtualization to integrate the Windows operating system (OS) with the Linux OS (running on an actual Linux kernel). You can add as many different Linux distributions as you please and run your favorite Linux tools, including GUI apps, alongside your favorite Windows tools. You can also mix and match Bash and PowerShell commands in the same command line. A Linux file system hosts your project files, while a separate Windows file systems hosts any developer tools, such as VS Code, that you\\\\u0027d like to work on the files with. This method of running Linux is highly efficient and performant. Once you choose a Linux distribution or opt to use the default Ubuntu distro, follow the instructions to Use the install Linux command with Windows Subsystem for Linux or learn more in the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl).\\\\n![WSL install command screenshot](https://learn.microsoft.com/en-us/linux/images/wsl-install-command-screenshot.png)\\\\n### Create a Virtual Machine (VM)\\\\nAvailable to everyone regardless if you\\\\u0027re using a Windows or MacOS-based machine, but will require either an Azure account, if creating a VM in the cloud, or virtualization software, if creating a VM directly on your machine (locally).\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/linux/install#step-1---choose-a-method-to-install-linux\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\nLinux is an operating system, similar to Windows, but with many different versions due to the nature of being open source and fully customizable. To install Linux, you must choose an install method and choose a Linux distribution.\\\\nTo install Linux:\\\\n1. Choose an install method: Windows Subsystem for Linux (WSL), Bare metal Linux; or create a Virtual Machine (VM) to run Linux locally or in the cloud.\\\\n2. Choose a Linux distribution: Ubuntu, Debian, Kali Linux, openSUSE, etc.\\\\n3. Follow the steps for your preferred install method:\\\\n3.1. Use the install Linux command with Windows Subsystem for Linux (WSL)\\\\n3.2. Create a Linux Virtual Machine (VM) in the cloud\\\\n3.3. Create a Linux Virtual Machine (VM) on your local machine\\\\n3.4. Create a bootable USB to install bare-metal Linux\\\\n4. After installing Linux: Get familiar with your distribution\\\\u0027s package manager, update and upgrade the packages available, and get familiar with the other [Linux resources at Microsoft](https://learn.microsoft.com/en-us/linux/), such as training courses, Linux-versions of popular tools, news, and Open Source events.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/linux/install\\u0022},{\\u0022title\\u0022:\\u0022Guide for changing your dev environment from Mac to Windows\\u0022,\\u0022content\\u0022:\\u0022# Guide for changing your dev environment from Mac to Windows\\\\n## Command-line shells and terminals\\\\nYou can now install Windows Subsystem for Linux (WSL) to support running a Linux shell within Windows. This means that you can run **bash**, with whichever specific Linux distribution you choose, integrated right inside Windows. Using WSL provides the kind of environment most familiar to Mac users. For example, you use **ls** to list the files in a current directory, not **dir** as you would with the traditional Windows Cmd Shell. To learn about installing and using WSL, see the [Windows Subsystem for Linux Installation Guide](https://learn.microsoft.com/en-us/windows/wsl/install). Linux distributions that you can install on Windows with WSL include:\\\\n1. [Ubuntu 20.04 LTS](https://www.microsoft.com/store/apps/9n6svws3rx71)\\\\n2. [Kali Linux](https://www.microsoft.com/store/apps/9PKR34TNCV07)\\\\n3. [Debian GNU/Linux](https://www.microsoft.com/store/apps/9MSVKQC78PK6)\\\\n4. [openSUSE Leap 15.1](https://www.microsoft.com/store/apps/9NJFZK00FGKV)\\\\n5. [SUSE Linux Enterprise Server 15 SP1](https://www.microsoft.com/store/apps/9PN498VPMF3Z)\\\\nJust to name a few. Find more in the [WSL install docs](https://learn.microsoft.com/en-us/windows/wsl/install-win10#install-your-linux-distribution-of-choice) and install them directly from the [Microsoft Store](https://aka.ms/wslstore).\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/windows/dev-environment/mac-to-windows#command-line-shells-and-terminals\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 3 - Follow install method instructions\\\\n### Install Linux with Windows Subsystem for Linux\\\\nTo install Linux on a Windows PC, use the install Linux command.\\\\n1. Open PowerShell or Windows Command Prompt in *administrator mode* by right-clicking and selecting \\\\u0022Run as administrator\\\\u0022.\\\\n2. Enter the Linux install command: \\\\u0060wsl --install\\\\u0060.\\\\n3. Restart your machine.\\\\n![WSL Linux Install List Command screenshot](https://learn.microsoft.com/en-us/linux/images/wsl-install-list-screenshot.png)\\\\n#### Install Linux command options\\\\nBy default, the install command will use the Ubuntu distribution of Linux. You can see the other distributions that are available in the Microsoft Store by entering the command: \\\\u0060wsl -l -o\\\\u0060. If you\\\\u0027d like to install a distribution other than Ubuntu, just add \\\\u0060-d\\\\u0060 to the command with the name of the distribution you\\\\u0027d like to install:\\\\n\\\\u0060\\\\u0060\\\\u0060powershell\\\\nwsl --install -d \\\\u003CDistro Name\\\\u003E\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nIf the distribution you want to install isn\\\\u0027t available in the Microsoft Store, see the Windows Subsystem for Linux (WSL) documentation on how to [Import any Linux distribution to use with WSL](https://learn.microsoft.com/en-us/windows/wsl/use-custom-distro).\\\\nFor more information on how to use VS Code, Git, databases, Docker, GUI apps, GPU acceleration, NodesJS, USB devices, or mount a disk, build a custom distribution, manage disk space, or set up WSL for your enterprise business, see the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl/).\\\\n### Create a Linux Virtual Machine in the cloud\\\\nTo create a Linux Virtual Machine (VM) that is hosted in the cloud on Azure, log-in to the Azure Portal and select the \\\\u201CCreate a Virtual Machine\\\\u201D service. Then follow the instructions for creating a user name, project, instance, SSH key, ports, and copying the IP address.\\\\nAzure supports a variety of options for setting up Linux VMs in the cloud. For a quickstart guide on how to set up a Linux VM, choose your preferred method:\\\\n1. [Azure Portal](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-portal?tabs=ubuntu\\\\u0026source=docs)\\\\n2. [Azure CLI](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-cli)\\\\n3. [Azure PowerShell](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-powershell)\\\\n4. [Terraform](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-terraform)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/linux/install#step-3---follow-install-method-instructions\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 2 - Choose a Linux distribution\\\\nA large user community that consistently contributes to the distribution, fixing bugs, adding features, helping with questions, etc. is another good consideration. Ubuntu is one of the most popular distos. openSUSE has been around longer than most other distributions and offers in addition to the stable version (Leap) a rolling version (Tumbleweed). Many other distros rapidly grow popular as the needs and preferences of the user community change. The Linux.org site has resources to help you assess what\\\\u0027s available, well-supported, or growing in popularity.\\\\n![Screenshot of the list of distributions on Linux.org](https://learn.microsoft.com/en-us/linux/images/linux-distributions-list-screenshot.png)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/linux/install#step-2---choose-a-linux-distribution\\u0022},{\\u0022title\\u0022:\\u0022Part 1.2 - Basic operations on Linux\\u0022,\\u0022content\\u0022:\\u0022# Part 1.2 - Basic operations on Linux\\\\n## Run commands as a superuser\\\\nTo become the root account user, run the \\\\u0060sudo su\\\\u0060 command.\\\\n![Screenshot of sudo su command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/sudo-su-command.png)\\\\nYou should notice two things: When you become root, the dollar sign character (\\\\u0060$\\\\u0060) becomes a pound sign character (\\\\u0060#\\\\u0060). Also, the username is changed to **root**. If you run the same \\\\u0060id\\\\u0060 command again, you see that the user and group ID of root are **0**.\\\\n![Screenshot of root command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/root-command.png)\\\\nImportant\\\\nNow you have elevated yourself to the \\\\u0022superuser\\\\u0022 role in your session, you have complete access to the system. Keep in mind that this is a potentially dangerous situation, and you should exercise caution as you continue.\\\\nNow, run the same command one more time:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\necho hello world \\\\u003E /etc/helloworld.txt\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nThis time, you don\\\\u0027t receive any error message. To verify if the file is created, run the following command:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\nll /etc/hello*\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nThe \\\\u0060ll\\\\u0060 command lists the files and subfolders that are in a specified folder. In this example, the \\\\u0060/etc/hello*\\\\u0060 parameter value limits the command output to the files or folders whose name begins in \\\\u0060hello\\\\u0060.\\\\n![Screenshot of hello command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/hello-command.png)\\\\nThe output shows that the file was created. What if we want to examine the content of the file? The \\\\u0060cat\\\\u0060 command helps here. To see the content, run \\\\u0060cat /etc/helloworld.txt\\\\u0060.\\\\n![Screenshot of cat command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/cat-command.png)\\\\nThe \\\\u0060cat\\\\u0060 command reads files sequentially, and writes their content to the standard output. Therefore, it writes \\\\u0022hello world\\\\u0022 in the console.\\\\nTo prevent mistakes that could damage the system while you\\\\u0027re elevated to root status, it\\\\u0027s a good practice to exit the root session and return to your standard user session to avoid some dangerous operations. To do this, run \\\\u0060exit\\\\u0060. You can see that the pound sign reverts to a dollar sign, and the username is displayed as your standard user.\\\\n![Screenshot of exit command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/exit-command.png)\\\\nFor additional practice, run the same \\\\u0060ll\\\\u0060 and \\\\u0060cat\\\\u0060 commands within your standard user context. You\\\\u0027ll see that you can\\\\u0027t create the file in the */etc/* folder, but can you list the file and read the contents.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\\u0022}]\"}]},\"id\":\"c4d77709f5314c9e9f097f08dd756438\",\"jsonrpc\":\"2.0\"}\n",
+       "data: {\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"{\\u0022results\\u0022:[{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 2 - Choose a Linux distribution\\\\nThe different versions of Linux are called \\\\u0022distributions\\\\u0022 (sometimes shortened to \\\\u201Cdistros\\\\u201D). So there isn\\\\u2019t actually a way to just \\\\u201Cinstall Linux\\\\u201D, you first need to choose which distribution that uses the Linux kernel you want to download and install. There are over 600 active Linux distributions, such as Ubuntu, Debian, Kali Linux, openSUSE, and more. There are a number of factors to consider in choosing a distribution, such as:\\\\n### Required Linux experience\\\\nIf you\\\\u2019re new to Linux, you may want to start with Ubuntu. This is one of the most common starting places for Linux users due to the level of support and ease of use. Ubuntu offers a desktop-version and a server-version depending on how you plan to use it. The specific tasks that the operating system will be responsible for may help you determine which distribution to use. Arch Linux is a popular choice for those who want a highly customizable, do-it-yourself approach that is still stable and well-supported by a large user base. It is a much more complicated place to start, but can help you to get a better understanding of how Linux works due to the amount of custom configuration.\\\\n### System requirements\\\\nA minimal, or \\\\u0022lightweight\\\\u0022, distribution means a small footprint, with lower memory and processing speed requirements. Alpine Linux is a popular option where users will find most features disabled or not installed by default, but still has everything you need for a containerized app. Security needs: Some Linux users may have specific security concerns to consider. Kali Linux is an example of a distro designed for security tasks, such as Penetration Testing, Security Research, Computer Forensics and Reverse Engineering.\\\\n### Business and Enterprise requirements\\\\nBusiness, corporate, or academic environments may have unique concerns from consumer environments. For example Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES) and Oracle Linux offer 24/7 global support and subscription-based service. CentOS is another popular enterprise distro that is community-supported but still compatible with RHEL.\\\\n### Community support\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\\u0022},{\\u0022title\\u0022:\\u0022Part 1.2 - Basic operations on Linux\\u0022,\\u0022content\\u0022:\\u0022# Part 1.2 - Basic operations on Linux\\\\n## Run commands by using the \\\\u0022sudo\\\\u0022 prefix\\\\nInstead of becoming the root account user in your session, you can run commands as root by using the \\\\u0060sudo \\\\u003Ccommand\\\\u003E\\\\u0060 format. This approach is safer because it will run the given command only as a superuser.\\\\nFor example, if you run \\\\u0060restart\\\\u0060 while you\\\\u0027re not elevated to superuser status, the result is as follows.\\\\n![Screenshot of restart command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/restart-command.png)\\\\nTo run this command as a superuser instead, you can either change the session context to root (by running \\\\u0060sudo su\\\\u0060) or add the \\\\u0060sudo\\\\u0060 prefix, as follows:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\nsudo reboot\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nNow, the restart operation runs as expected by the virtual machine.\\\\n![Screenshot of reboot command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/reboot-command.png)\\\\n## Package managers\\\\nPackage managers are used to install, upgrade, and remove applications in Linux. There are several different package managers, such as DPKG (Debian Package Management System), RPM (Red Hat Package Manager), and Zypper for SUSE Package managers themselves are low-level. Therefore, it might be difficult to manage software directly by using the package manager. Fortunately, there are wrappers for these tools that make it easier to install and uninstall software. For example, [yum](https://wikipedia.org/wiki/Yum_%28software%29) is a wrapper around RPM (Red Hat Package Manager) and [APT](https://wikipedia.org/wiki/APT_%28software%29) is a wrapper around DPKG (Debian Package Management System).\\\\nThis tutorial assumes that you\\\\u0027re running the [Ubuntu package management system](https://ubuntu.com/server/docs/package-management). This system is derived from the same system that\\\\u0027s used by the Debian GNU/Linux distribution. Therefore, we can use APT to install the software.\\\\nDepending on your Linux choice, you might have to use other tools. For example, you can use yum to install or uninstall software if you\\\\u0027re running Red Hat.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-by-using-the-sudo-prefix\\u0022},{\\u0022title\\u0022:\\u0022Part 1.2 - Basic operations on Linux\\u0022,\\u0022content\\u0022:\\u0022# Part 1.2 - Basic operations on Linux\\\\n## Run commands as a superuser\\\\nThe superuser is the most privileged user account. It has root (unrestricted) access to all files and folders, and complete control over the operation of the computer. The system administrator uses this account for system maintenance.\\\\nOn UNIX-like systems, the conventional name of the superuser is \\\\u0022root\\\\u0022. The root user can do tasks that are otherwise restricted for standard users, including the following ones:\\\\n1. Changing system directories\\\\n2. Changing user privileges\\\\n3. Changing file ownership\\\\n4. System-related tasks such as restarts\\\\n5. Installation of certain applications\\\\nThere are several ways to run commands as the root account user. One is to use the \\\\u0060sudo su\\\\u0060 command. In this command, \\\\u0060sudo\\\\u0060 is short for \\\\u0060superuser do\\\\u0060, and \\\\u0060su\\\\u0060 is an abbreviation for \\\\u0022switch user.\\\\u0022 You can also use the \\\\u0060su\\\\u0060 command to switch between standard users.\\\\nImportant\\\\nAfter you become the root account user, everything you do runs in root context. Therefore, you should act carefully because the root account has access to the entire system.\\\\nTo demonstrate a permissions issue, create a text file in a special folder that\\\\u0027s named */etc*.\\\\nThe */etc/* folder is where system configuration files are located. This folder protects its contents from being changed by a standard user.\\\\nRun the following command:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\necho hello world \\\\u003E /etc/helloworld.txt\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nThe \\\\u0060echo\\\\u0060 command writes all the text that follows it to the output. The angle bracket (\\\\u0060\\\\u003E\\\\u0060) tells the system to send the output to the */etc/helloworld.txt* file instead of the console. This behavior is similar to how Windows works.\\\\nNote\\\\nLinux is case-sensitive: *Helloworld.txt*, *helloworld.txt* and *helloworld.Txt* are different files.\\\\nYou don\\\\u0027t have to provide a file name extension. The name \\\\u0022helloworld\\\\u0022 by itself is perfectly valid. (The \\\\u0022txt\\\\u0022 extension is used here only as an example.)\\\\nWhen you run this command, you receive a **permission denied** error message.\\\\n![Screenshot of permission denied message.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/permission-denied-message.png)\\\\nThe operation fails because the */etc* folder is special folder that a standard user can\\\\u0027t change. Verify the user by running the \\\\u0060id\\\\u0060 command.\\\\n![Screenshot of I D command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/id-command.png)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 1 - Choose a method to install Linux\\\\nThe choice of which method to use for installing Linux depends on your needs and preferences.\\\\n1. **New to using Linux?** We recommend starting with Windows Subsystem for Linux (WSL) as it\\\\u0027s the easiest way to get up and running.\\\\n2. **Work in a business environment with more complex needs related to scale or security?** We recommend running Linux as a Virtual Machine (VM) in the cloud and checking out the support that Azure has to offer. This also applies if you want to run Linux as a server.\\\\n3. **Only want to run Linux as your primary operating system?** If you are good with a slightly more complicated install process and don\\\\u0027t need access to Windows tools (like Outlook, Teams, Word, PowerPoint, etc), you can run Linux on bare metal to access the full potential of your hardware without any overhead from virtualization or emulation.\\\\nLearn more about these options below.\\\\n### Windows Subsystem for Linux (WSL)\\\\nAvailable to those with a Windows machine, this is the most simple way to install Linux. Just run the Linux install command: \\\\u0060wsl --install\\\\u0060 to install the Ubuntu distribution.\\\\nThis method uses virtualization to integrate the Windows operating system (OS) with the Linux OS (running on an actual Linux kernel). You can add as many different Linux distributions as you please and run your favorite Linux tools, including GUI apps, alongside your favorite Windows tools. You can also mix and match Bash and PowerShell commands in the same command line. A Linux file system hosts your project files, while a separate Windows file systems hosts any developer tools, such as VS Code, that you\\\\u0027d like to work on the files with. This method of running Linux is highly efficient and performant. Once you choose a Linux distribution or opt to use the default Ubuntu distro, follow the instructions to Use the install Linux command with Windows Subsystem for Linux or learn more in the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl).\\\\n![WSL install command screenshot](https://learn.microsoft.com/linux/images/wsl-install-command-screenshot.png)\\\\n### Create a Virtual Machine (VM)\\\\nAvailable to everyone regardless if you\\\\u0027re using a Windows or MacOS-based machine, but will require either an Azure account, if creating a VM in the cloud, or virtualization software, if creating a VM directly on your machine (locally).\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/linux/install#step-1---choose-a-method-to-install-linux\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\nLinux is an operating system, similar to Windows, but with many different versions due to the nature of being open source and fully customizable. To install Linux, you must choose an install method and choose a Linux distribution.\\\\nTo install Linux:\\\\n1. Choose an install method: Windows Subsystem for Linux (WSL), Bare metal Linux; or create a Virtual Machine (VM) to run Linux locally or in the cloud.\\\\n2. Choose a Linux distribution: Ubuntu, Debian, Kali Linux, openSUSE, etc.\\\\n3. Follow the steps for your preferred install method:\\\\n3.1. Use the install Linux command with Windows Subsystem for Linux (WSL)\\\\n3.2. Create a Linux Virtual Machine (VM) in the cloud\\\\n3.3. Create a Linux Virtual Machine (VM) on your local machine\\\\n3.4. Create a bootable USB to install bare-metal Linux\\\\n4. After installing Linux: Get familiar with your distribution\\\\u0027s package manager, update and upgrade the packages available, and get familiar with the other [Linux resources at Microsoft](https://learn.microsoft.com/linux/), such as training courses, Linux-versions of popular tools, news, and Open Source events.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/linux/install\\u0022},{\\u0022title\\u0022:\\u0022New to Linux resources for SQL users\\u0022,\\u0022content\\u0022:\\u0022# New to Linux resources for SQL users\\\\n**Applies to:**![](https://learn.microsoft.com/sql/includes/media/yes-icon.svg?view=sql-server-ver17)[SQL Server](https://learn.microsoft.com/sql/sql-server/sql-docs-navigation-guide?view=sql-server-ver17#applies-to) on Linux\\\\nThis article provides a learning path through an introduction to Linux concepts. Use the sections in the article as a guided learning path to familiarize yourself with the Linux environment.\\\\nThis list isn\\\\u0027t comprehensive but instead provides the bare minimum you need to help you administer and navigate your SQL Server on Linux environment. See the [full list of tutorials](https://www.linux.org/forums/linux-beginner-tutorials.123/) to learn more.\\\\n## What is Linux?\\\\nThe [What is Linux](https://www.linux.org/threads/what-is-linux.4106/) module introduces the history of Linux as an operating system. The module explains *the Kernel* and where Linux is today. This tutorial serves as an introduction to Linux, helping you get started.\\\\n## Select a distribution\\\\nAfter you know the history of Linux, decide which [distribution of Linux](https://www.linux.org/threads/selecting-a-linux-distribution.4117/) best suits your business needs.\\\\nSQL Server 2025 (17.x) is [supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2025?view=sql-server-ver17#supported-platforms) on Red Hat Enterprise Linux (RHEL) and Ubuntu.\\\\nSQL Server 2022 (16.x) and earlier versions are [also supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2022?view=sql-server-ver17#supported-platforms) on SUSE Linux Enterprise Server (SLES).\\\\n## Get around directories\\\\nAfter you choose the appropriate distribution of Linux, familiarize yourself with [getting around Linux directories](https://www.linux.org/threads/getting-around-in-linux-directories.4120/).\\\\nThis module helps you answer the following questions:\\\\n1. How to navigate between different files\\\\n2. How to identify a file from a directory\\\\n3. Identify different directories\\\\n## Install new software\\\\nNext, learn how to [install new software](https://www.linux.org/threads/installing-new-software-debian-red-hat-slackware.4119/) on your new Linux operating system. This module covers installing new software on the Debian and Red Hat Linux operating systems.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/sql/linux/new-to-linux-resources?view=sql-server-ver17\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 3 - Follow install method instructions\\\\n### Install Linux with Windows Subsystem for Linux\\\\nTo install Linux on a Windows PC, use the install Linux command.\\\\n1. Open PowerShell or Windows Command Prompt in *administrator mode* by right-clicking and selecting \\\\u0022Run as administrator\\\\u0022.\\\\n2. Enter the Linux install command: \\\\u0060wsl --install\\\\u0060.\\\\n3. Restart your machine.\\\\n![WSL Linux Install List Command screenshot](https://learn.microsoft.com/linux/images/wsl-install-list-screenshot.png)\\\\n#### Install Linux command options\\\\nBy default, the install command will use the Ubuntu distribution of Linux. You can see the other distributions that are available in the Microsoft Store by entering the command: \\\\u0060wsl -l -o\\\\u0060. If you\\\\u0027d like to install a distribution other than Ubuntu, just add \\\\u0060-d\\\\u0060 to the command with the name of the distribution you\\\\u0027d like to install:\\\\n\\\\u0060\\\\u0060\\\\u0060powershell\\\\nwsl --install -d \\\\u003CDistro Name\\\\u003E\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nIf the distribution you want to install isn\\\\u0027t available in the Microsoft Store, see the Windows Subsystem for Linux (WSL) documentation on how to [Import any Linux distribution to use with WSL](https://learn.microsoft.com/en-us/windows/wsl/use-custom-distro).\\\\nFor more information on how to use VS Code, Git, databases, Docker, GUI apps, GPU acceleration, NodesJS, USB devices, or mount a disk, build a custom distribution, manage disk space, or set up WSL for your enterprise business, see the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl/).\\\\n### Create a Linux Virtual Machine in the cloud\\\\nTo create a Linux Virtual Machine (VM) that is hosted in the cloud on Azure, log-in to the Azure Portal and select the \\\\u201CCreate a Virtual Machine\\\\u201D service. Then follow the instructions for creating a user name, project, instance, SSH key, ports, and copying the IP address.\\\\nAzure supports a variety of options for setting up Linux VMs in the cloud. For a quickstart guide on how to set up a Linux VM, choose your preferred method:\\\\n1. [Azure Portal](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-portal?tabs=ubuntu\\\\u0026source=docs)\\\\n2. [Azure CLI](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-cli)\\\\n3. [Azure PowerShell](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-powershell)\\\\n4. [Terraform](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-terraform)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/linux/install#step-3---follow-install-method-instructions\\u0022},{\\u0022title\\u0022:\\u0022How to download and install Linux\\u0022,\\u0022content\\u0022:\\u0022# How to download and install Linux\\\\n## Step 2 - Choose a Linux distribution\\\\nA large user community that consistently contributes to the distribution, fixing bugs, adding features, helping with questions, etc. is another good consideration. Ubuntu is one of the most popular distos. openSUSE has been around longer than most other distributions and offers in addition to the stable version (Leap) a rolling version (Tumbleweed). Many other distros rapidly grow popular as the needs and preferences of the user community change. The Linux.org site has resources to help you assess what\\\\u0027s available, well-supported, or growing in popularity.\\\\n![Screenshot of the list of distributions on Linux.org](https://learn.microsoft.com/linux/images/linux-distributions-list-screenshot.png)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\\u0022},{\\u0022title\\u0022:\\u0022Part 1.2 - Basic operations on Linux\\u0022,\\u0022content\\u0022:\\u0022# Part 1.2 - Basic operations on Linux\\\\n## Run commands as a superuser\\\\nTo become the root account user, run the \\\\u0060sudo su\\\\u0060 command.\\\\n![Screenshot of sudo su command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/sudo-su-command.png)\\\\nYou should notice two things: When you become root, the dollar sign character (\\\\u0060$\\\\u0060) becomes a pound sign character (\\\\u0060#\\\\u0060). Also, the username is changed to **root**. If you run the same \\\\u0060id\\\\u0060 command again, you see that the user and group ID of root are **0**.\\\\n![Screenshot of root command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/root-command.png)\\\\nImportant\\\\nNow you have elevated yourself to the \\\\u0022superuser\\\\u0022 role in your session, you have complete access to the system. Keep in mind that this is a potentially dangerous situation, and you should exercise caution as you continue.\\\\nNow, run the same command one more time:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\necho hello world \\\\u003E /etc/helloworld.txt\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nThis time, you don\\\\u0027t receive any error message. To verify if the file is created, run the following command:\\\\n\\\\u0060\\\\u0060\\\\u0060bash\\\\nll /etc/hello*\\\\n\\\\u0060\\\\u0060\\\\u0060\\\\nThe \\\\u0060ll\\\\u0060 command lists the files and subfolders that are in a specified folder. In this example, the \\\\u0060/etc/hello*\\\\u0060 parameter value limits the command output to the files or folders whose name begins in \\\\u0060hello\\\\u0060.\\\\n![Screenshot of hello command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/hello-command.png)\\\\nThe output shows that the file was created. What if we want to examine the content of the file? The \\\\u0060cat\\\\u0060 command helps here. To see the content, run \\\\u0060cat /etc/helloworld.txt\\\\u0060.\\\\n![Screenshot of cat command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/cat-command.png)\\\\nThe \\\\u0060cat\\\\u0060 command reads files sequentially, and writes their content to the standard output. Therefore, it writes \\\\u0022hello world\\\\u0022 in the console.\\\\nTo prevent mistakes that could damage the system while you\\\\u0027re elevated to root status, it\\\\u0027s a good practice to exit the root session and return to your standard user session to avoid some dangerous operations. To do this, run \\\\u0060exit\\\\u0060. You can see that the pound sign reverts to a dollar sign, and the username is displayed as your standard user.\\\\n![Screenshot of exit command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/exit-command.png)\\\\nFor additional practice, run the same \\\\u0060ll\\\\u0060 and \\\\u0060cat\\\\u0060 commands within your standard user context. You\\\\u0027ll see that you can\\\\u0027t create the file in the */etc/* folder, but can you list the file and read the contents.\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\\u0022},{\\u0022title\\u0022:\\u0022Part 1.2 - Basic operations on Linux\\u0022,\\u0022content\\u0022:\\u0022# Part 1.2 - Basic operations on Linux\\\\n## Remove packages\\\\nFinding the file doesn\\\\u0027t involve package managers. Instead, you\\\\u0027ll run a standard search for files in Linux. There are several ways to do so. One of the most common search commands is \\\\u0060find\\\\u0060. However, this command is also confusing to use, so we won\\\\u0027t discuss it here. Instead, we\\\\u0027ll use \\\\u0060whereis\\\\u0060.\\\\nNote\\\\nLinux is very well documented. Nearly every command has a useful help page available. Use the [man](https://www.geeksforgeeks.org/man-command-in-linux-with-examples/) and [info](https://www.geeksforgeeks.org/info-command-in-linux-with-examples/) commands to show the Help pages in Linux. For example, to learn more on your own about the \\\\u0060find\\\\u0060 command, run \\\\u0060man find\\\\u0060 or \\\\u0060info find\\\\u0060.\\\\nAccording to the Help page for \\\\u0060whereis\\\\u0060, the definition is as follows:\\\\n\\\\u0060whereis\\\\u0060 locates the binary, source, and manual files for the specified command names.\\\\nIf you run \\\\u0060whereis apache2\\\\u0060, you should find the */etc/apache2* installation.\\\\n![Screenshot of whereis apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/whereis-apache2-command.png)\\\\nRemember that the */etc/* directory is where the system configuration files are located. To learn whether \\\\u0022apache2\\\\u0022is a file or folder, run \\\\u0060ll /etc/apache2\\\\u0060. As you can see, this is a folder, and it contains the apache2 configuration files.\\\\n![Screenshot of ll etc apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/ll-etc-apache2-command.png)\\\\nYou expect \\\\u0060apache2\\\\u0060 to contain these files and folders because you used the \\\\u0060apt remove\\\\u0060 command that doesn\\\\u0027t delete the configuration files.\\\\nAs final step, run the \\\\u0060apt purge\\\\u0060 command to see its effect. You can run a purge to clean up the installation folder even after you remove the package. The following screenshot shows that \\\\u0060whereis\\\\u0060 didn\\\\u0027t find anything. This is because the \\\\u0060purge\\\\u0060 command removed the configuration files together with the binaries.\\\\n![Screenshot of apt purge command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/apt-purge-command.png)\\u0022,\\u0022contentUrl\\u0022:\\u0022https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#remove-packages\\u0022}]}\"}],\"structuredContent\":{\"results\":[{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 2 - Choose a Linux distribution\\nThe different versions of Linux are called \\u0022distributions\\u0022 (sometimes shortened to \\u201Cdistros\\u201D). So there isn\\u2019t actually a way to just \\u201Cinstall Linux\\u201D, you first need to choose which distribution that uses the Linux kernel you want to download and install. There are over 600 active Linux distributions, such as Ubuntu, Debian, Kali Linux, openSUSE, and more. There are a number of factors to consider in choosing a distribution, such as:\\n### Required Linux experience\\nIf you\\u2019re new to Linux, you may want to start with Ubuntu. This is one of the most common starting places for Linux users due to the level of support and ease of use. Ubuntu offers a desktop-version and a server-version depending on how you plan to use it. The specific tasks that the operating system will be responsible for may help you determine which distribution to use. Arch Linux is a popular choice for those who want a highly customizable, do-it-yourself approach that is still stable and well-supported by a large user base. It is a much more complicated place to start, but can help you to get a better understanding of how Linux works due to the amount of custom configuration.\\n### System requirements\\nA minimal, or \\u0022lightweight\\u0022, distribution means a small footprint, with lower memory and processing speed requirements. Alpine Linux is a popular option where users will find most features disabled or not installed by default, but still has everything you need for a containerized app. Security needs: Some Linux users may have specific security concerns to consider. Kali Linux is an example of a distro designed for security tasks, such as Penetration Testing, Security Research, Computer Forensics and Reverse Engineering.\\n### Business and Enterprise requirements\\nBusiness, corporate, or academic environments may have unique concerns from consumer environments. For example Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES) and Oracle Linux offer 24/7 global support and subscription-based service. CentOS is another popular enterprise distro that is community-supported but still compatible with RHEL.\\n### Community support\",\"contentUrl\":\"https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\"},{\"title\":\"Part 1.2 - Basic operations on Linux\",\"content\":\"# Part 1.2 - Basic operations on Linux\\n## Run commands by using the \\u0022sudo\\u0022 prefix\\nInstead of becoming the root account user in your session, you can run commands as root by using the \\u0060sudo \\u003Ccommand\\u003E\\u0060 format. This approach is safer because it will run the given command only as a superuser.\\nFor example, if you run \\u0060restart\\u0060 while you\\u0027re not elevated to superuser status, the result is as follows.\\n![Screenshot of restart command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/restart-command.png)\\nTo run this command as a superuser instead, you can either change the session context to root (by running \\u0060sudo su\\u0060) or add the \\u0060sudo\\u0060 prefix, as follows:\\n\\u0060\\u0060\\u0060bash\\nsudo reboot\\n\\u0060\\u0060\\u0060\\nNow, the restart operation runs as expected by the virtual machine.\\n![Screenshot of reboot command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/reboot-command.png)\\n## Package managers\\nPackage managers are used to install, upgrade, and remove applications in Linux. There are several different package managers, such as DPKG (Debian Package Management System), RPM (Red Hat Package Manager), and Zypper for SUSE Package managers themselves are low-level. Therefore, it might be difficult to manage software directly by using the package manager. Fortunately, there are wrappers for these tools that make it easier to install and uninstall software. For example, [yum](https://wikipedia.org/wiki/Yum_%28software%29) is a wrapper around RPM (Red Hat Package Manager) and [APT](https://wikipedia.org/wiki/APT_%28software%29) is a wrapper around DPKG (Debian Package Management System).\\nThis tutorial assumes that you\\u0027re running the [Ubuntu package management system](https://ubuntu.com/server/docs/package-management). This system is derived from the same system that\\u0027s used by the Debian GNU/Linux distribution. Therefore, we can use APT to install the software.\\nDepending on your Linux choice, you might have to use other tools. For example, you can use yum to install or uninstall software if you\\u0027re running Red Hat.\",\"contentUrl\":\"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-by-using-the-sudo-prefix\"},{\"title\":\"Part 1.2 - Basic operations on Linux\",\"content\":\"# Part 1.2 - Basic operations on Linux\\n## Run commands as a superuser\\nThe superuser is the most privileged user account. It has root (unrestricted) access to all files and folders, and complete control over the operation of the computer. The system administrator uses this account for system maintenance.\\nOn UNIX-like systems, the conventional name of the superuser is \\u0022root\\u0022. The root user can do tasks that are otherwise restricted for standard users, including the following ones:\\n1. Changing system directories\\n2. Changing user privileges\\n3. Changing file ownership\\n4. System-related tasks such as restarts\\n5. Installation of certain applications\\nThere are several ways to run commands as the root account user. One is to use the \\u0060sudo su\\u0060 command. In this command, \\u0060sudo\\u0060 is short for \\u0060superuser do\\u0060, and \\u0060su\\u0060 is an abbreviation for \\u0022switch user.\\u0022 You can also use the \\u0060su\\u0060 command to switch between standard users.\\nImportant\\nAfter you become the root account user, everything you do runs in root context. Therefore, you should act carefully because the root account has access to the entire system.\\nTo demonstrate a permissions issue, create a text file in a special folder that\\u0027s named */etc*.\\nThe */etc/* folder is where system configuration files are located. This folder protects its contents from being changed by a standard user.\\nRun the following command:\\n\\u0060\\u0060\\u0060bash\\necho hello world \\u003E /etc/helloworld.txt\\n\\u0060\\u0060\\u0060\\nThe \\u0060echo\\u0060 command writes all the text that follows it to the output. The angle bracket (\\u0060\\u003E\\u0060) tells the system to send the output to the */etc/helloworld.txt* file instead of the console. This behavior is similar to how Windows works.\\nNote\\nLinux is case-sensitive: *Helloworld.txt*, *helloworld.txt* and *helloworld.Txt* are different files.\\nYou don\\u0027t have to provide a file name extension. The name \\u0022helloworld\\u0022 by itself is perfectly valid. (The \\u0022txt\\u0022 extension is used here only as an example.)\\nWhen you run this command, you receive a **permission denied** error message.\\n![Screenshot of permission denied message.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/permission-denied-message.png)\\nThe operation fails because the */etc* folder is special folder that a standard user can\\u0027t change. Verify the user by running the \\u0060id\\u0060 command.\\n![Screenshot of I D command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/id-command.png)\",\"contentUrl\":\"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 1 - Choose a method to install Linux\\nThe choice of which method to use for installing Linux depends on your needs and preferences.\\n1. **New to using Linux?** We recommend starting with Windows Subsystem for Linux (WSL) as it\\u0027s the easiest way to get up and running.\\n2. **Work in a business environment with more complex needs related to scale or security?** We recommend running Linux as a Virtual Machine (VM) in the cloud and checking out the support that Azure has to offer. This also applies if you want to run Linux as a server.\\n3. **Only want to run Linux as your primary operating system?** If you are good with a slightly more complicated install process and don\\u0027t need access to Windows tools (like Outlook, Teams, Word, PowerPoint, etc), you can run Linux on bare metal to access the full potential of your hardware without any overhead from virtualization or emulation.\\nLearn more about these options below.\\n### Windows Subsystem for Linux (WSL)\\nAvailable to those with a Windows machine, this is the most simple way to install Linux. Just run the Linux install command: \\u0060wsl --install\\u0060 to install the Ubuntu distribution.\\nThis method uses virtualization to integrate the Windows operating system (OS) with the Linux OS (running on an actual Linux kernel). You can add as many different Linux distributions as you please and run your favorite Linux tools, including GUI apps, alongside your favorite Windows tools. You can also mix and match Bash and PowerShell commands in the same command line. A Linux file system hosts your project files, while a separate Windows file systems hosts any developer tools, such as VS Code, that you\\u0027d like to work on the files with. This method of running Linux is highly efficient and performant. Once you choose a Linux distribution or opt to use the default Ubuntu distro, follow the instructions to Use the install Linux command with Windows Subsystem for Linux or learn more in the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl).\\n![WSL install command screenshot](https://learn.microsoft.com/linux/images/wsl-install-command-screenshot.png)\\n### Create a Virtual Machine (VM)\\nAvailable to everyone regardless if you\\u0027re using a Windows or MacOS-based machine, but will require either an Azure account, if creating a VM in the cloud, or virtualization software, if creating a VM directly on your machine (locally).\",\"contentUrl\":\"https://learn.microsoft.com/linux/install#step-1---choose-a-method-to-install-linux\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\nLinux is an operating system, similar to Windows, but with many different versions due to the nature of being open source and fully customizable. To install Linux, you must choose an install method and choose a Linux distribution.\\nTo install Linux:\\n1. Choose an install method: Windows Subsystem for Linux (WSL), Bare metal Linux; or create a Virtual Machine (VM) to run Linux locally or in the cloud.\\n2. Choose a Linux distribution: Ubuntu, Debian, Kali Linux, openSUSE, etc.\\n3. Follow the steps for your preferred install method:\\n3.1. Use the install Linux command with Windows Subsystem for Linux (WSL)\\n3.2. Create a Linux Virtual Machine (VM) in the cloud\\n3.3. Create a Linux Virtual Machine (VM) on your local machine\\n3.4. Create a bootable USB to install bare-metal Linux\\n4. After installing Linux: Get familiar with your distribution\\u0027s package manager, update and upgrade the packages available, and get familiar with the other [Linux resources at Microsoft](https://learn.microsoft.com/linux/), such as training courses, Linux-versions of popular tools, news, and Open Source events.\",\"contentUrl\":\"https://learn.microsoft.com/linux/install\"},{\"title\":\"New to Linux resources for SQL users\",\"content\":\"# New to Linux resources for SQL users\\n**Applies to:**![](https://learn.microsoft.com/sql/includes/media/yes-icon.svg?view=sql-server-ver17)[SQL Server](https://learn.microsoft.com/sql/sql-server/sql-docs-navigation-guide?view=sql-server-ver17#applies-to) on Linux\\nThis article provides a learning path through an introduction to Linux concepts. Use the sections in the article as a guided learning path to familiarize yourself with the Linux environment.\\nThis list isn\\u0027t comprehensive but instead provides the bare minimum you need to help you administer and navigate your SQL Server on Linux environment. See the [full list of tutorials](https://www.linux.org/forums/linux-beginner-tutorials.123/) to learn more.\\n## What is Linux?\\nThe [What is Linux](https://www.linux.org/threads/what-is-linux.4106/) module introduces the history of Linux as an operating system. The module explains *the Kernel* and where Linux is today. This tutorial serves as an introduction to Linux, helping you get started.\\n## Select a distribution\\nAfter you know the history of Linux, decide which [distribution of Linux](https://www.linux.org/threads/selecting-a-linux-distribution.4117/) best suits your business needs.\\nSQL Server 2025 (17.x) is [supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2025?view=sql-server-ver17#supported-platforms) on Red Hat Enterprise Linux (RHEL) and Ubuntu.\\nSQL Server 2022 (16.x) and earlier versions are [also supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2022?view=sql-server-ver17#supported-platforms) on SUSE Linux Enterprise Server (SLES).\\n## Get around directories\\nAfter you choose the appropriate distribution of Linux, familiarize yourself with [getting around Linux directories](https://www.linux.org/threads/getting-around-in-linux-directories.4120/).\\nThis module helps you answer the following questions:\\n1. How to navigate between different files\\n2. How to identify a file from a directory\\n3. Identify different directories\\n## Install new software\\nNext, learn how to [install new software](https://www.linux.org/threads/installing-new-software-debian-red-hat-slackware.4119/) on your new Linux operating system. This module covers installing new software on the Debian and Red Hat Linux operating systems.\",\"contentUrl\":\"https://learn.microsoft.com/sql/linux/new-to-linux-resources?view=sql-server-ver17\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 3 - Follow install method instructions\\n### Install Linux with Windows Subsystem for Linux\\nTo install Linux on a Windows PC, use the install Linux command.\\n1. Open PowerShell or Windows Command Prompt in *administrator mode* by right-clicking and selecting \\u0022Run as administrator\\u0022.\\n2. Enter the Linux install command: \\u0060wsl --install\\u0060.\\n3. Restart your machine.\\n![WSL Linux Install List Command screenshot](https://learn.microsoft.com/linux/images/wsl-install-list-screenshot.png)\\n#### Install Linux command options\\nBy default, the install command will use the Ubuntu distribution of Linux. You can see the other distributions that are available in the Microsoft Store by entering the command: \\u0060wsl -l -o\\u0060. If you\\u0027d like to install a distribution other than Ubuntu, just add \\u0060-d\\u0060 to the command with the name of the distribution you\\u0027d like to install:\\n\\u0060\\u0060\\u0060powershell\\nwsl --install -d \\u003CDistro Name\\u003E\\n\\u0060\\u0060\\u0060\\nIf the distribution you want to install isn\\u0027t available in the Microsoft Store, see the Windows Subsystem for Linux (WSL) documentation on how to [Import any Linux distribution to use with WSL](https://learn.microsoft.com/en-us/windows/wsl/use-custom-distro).\\nFor more information on how to use VS Code, Git, databases, Docker, GUI apps, GPU acceleration, NodesJS, USB devices, or mount a disk, build a custom distribution, manage disk space, or set up WSL for your enterprise business, see the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl/).\\n### Create a Linux Virtual Machine in the cloud\\nTo create a Linux Virtual Machine (VM) that is hosted in the cloud on Azure, log-in to the Azure Portal and select the \\u201CCreate a Virtual Machine\\u201D service. Then follow the instructions for creating a user name, project, instance, SSH key, ports, and copying the IP address.\\nAzure supports a variety of options for setting up Linux VMs in the cloud. For a quickstart guide on how to set up a Linux VM, choose your preferred method:\\n1. [Azure Portal](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-portal?tabs=ubuntu\\u0026source=docs)\\n2. [Azure CLI](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-cli)\\n3. [Azure PowerShell](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-powershell)\\n4. [Terraform](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-terraform)\",\"contentUrl\":\"https://learn.microsoft.com/linux/install#step-3---follow-install-method-instructions\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 2 - Choose a Linux distribution\\nA large user community that consistently contributes to the distribution, fixing bugs, adding features, helping with questions, etc. is another good consideration. Ubuntu is one of the most popular distos. openSUSE has been around longer than most other distributions and offers in addition to the stable version (Leap) a rolling version (Tumbleweed). Many other distros rapidly grow popular as the needs and preferences of the user community change. The Linux.org site has resources to help you assess what\\u0027s available, well-supported, or growing in popularity.\\n![Screenshot of the list of distributions on Linux.org](https://learn.microsoft.com/linux/images/linux-distributions-list-screenshot.png)\",\"contentUrl\":\"https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\"},{\"title\":\"Part 1.2 - Basic operations on Linux\",\"content\":\"# Part 1.2 - Basic operations on Linux\\n## Run commands as a superuser\\nTo become the root account user, run the \\u0060sudo su\\u0060 command.\\n![Screenshot of sudo su command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/sudo-su-command.png)\\nYou should notice two things: When you become root, the dollar sign character (\\u0060$\\u0060) becomes a pound sign character (\\u0060#\\u0060). Also, the username is changed to **root**. If you run the same \\u0060id\\u0060 command again, you see that the user and group ID of root are **0**.\\n![Screenshot of root command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/root-command.png)\\nImportant\\nNow you have elevated yourself to the \\u0022superuser\\u0022 role in your session, you have complete access to the system. Keep in mind that this is a potentially dangerous situation, and you should exercise caution as you continue.\\nNow, run the same command one more time:\\n\\u0060\\u0060\\u0060bash\\necho hello world \\u003E /etc/helloworld.txt\\n\\u0060\\u0060\\u0060\\nThis time, you don\\u0027t receive any error message. To verify if the file is created, run the following command:\\n\\u0060\\u0060\\u0060bash\\nll /etc/hello*\\n\\u0060\\u0060\\u0060\\nThe \\u0060ll\\u0060 command lists the files and subfolders that are in a specified folder. In this example, the \\u0060/etc/hello*\\u0060 parameter value limits the command output to the files or folders whose name begins in \\u0060hello\\u0060.\\n![Screenshot of hello command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/hello-command.png)\\nThe output shows that the file was created. What if we want to examine the content of the file? The \\u0060cat\\u0060 command helps here. To see the content, run \\u0060cat /etc/helloworld.txt\\u0060.\\n![Screenshot of cat command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/cat-command.png)\\nThe \\u0060cat\\u0060 command reads files sequentially, and writes their content to the standard output. Therefore, it writes \\u0022hello world\\u0022 in the console.\\nTo prevent mistakes that could damage the system while you\\u0027re elevated to root status, it\\u0027s a good practice to exit the root session and return to your standard user session to avoid some dangerous operations. To do this, run \\u0060exit\\u0060. You can see that the pound sign reverts to a dollar sign, and the username is displayed as your standard user.\\n![Screenshot of exit command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/exit-command.png)\\nFor additional practice, run the same \\u0060ll\\u0060 and \\u0060cat\\u0060 commands within your standard user context. You\\u0027ll see that you can\\u0027t create the file in the */etc/* folder, but can you list the file and read the contents.\",\"contentUrl\":\"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\"},{\"title\":\"Part 1.2 - Basic operations on Linux\",\"content\":\"# Part 1.2 - Basic operations on Linux\\n## Remove packages\\nFinding the file doesn\\u0027t involve package managers. Instead, you\\u0027ll run a standard search for files in Linux. There are several ways to do so. One of the most common search commands is \\u0060find\\u0060. However, this command is also confusing to use, so we won\\u0027t discuss it here. Instead, we\\u0027ll use \\u0060whereis\\u0060.\\nNote\\nLinux is very well documented. Nearly every command has a useful help page available. Use the [man](https://www.geeksforgeeks.org/man-command-in-linux-with-examples/) and [info](https://www.geeksforgeeks.org/info-command-in-linux-with-examples/) commands to show the Help pages in Linux. For example, to learn more on your own about the \\u0060find\\u0060 command, run \\u0060man find\\u0060 or \\u0060info find\\u0060.\\nAccording to the Help page for \\u0060whereis\\u0060, the definition is as follows:\\n\\u0060whereis\\u0060 locates the binary, source, and manual files for the specified command names.\\nIf you run \\u0060whereis apache2\\u0060, you should find the */etc/apache2* installation.\\n![Screenshot of whereis apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/whereis-apache2-command.png)\\nRemember that the */etc/* directory is where the system configuration files are located. To learn whether \\u0022apache2\\u0022is a file or folder, run \\u0060ll /etc/apache2\\u0060. As you can see, this is a folder, and it contains the apache2 configuration files.\\n![Screenshot of ll etc apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/ll-etc-apache2-command.png)\\nYou expect \\u0060apache2\\u0060 to contain these files and folders because you used the \\u0060apt remove\\u0060 command that doesn\\u0027t delete the configuration files.\\nAs final step, run the \\u0060apt purge\\u0060 command to see its effect. You can run a purge to clean up the installation folder even after you remove the package. The following screenshot shows that \\u0060whereis\\u0060 didn\\u0027t find anything. This is because the \\u0060purge\\u0060 command removed the configuration files together with the binaries.\\n![Screenshot of apt purge command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/apt-purge-command.png)\",\"contentUrl\":\"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#remove-packages\"}]}},\"id\":\"07e26d19ee6248d98f6dc039bf4fc589\",\"jsonrpc\":\"2.0\"}\n",
        "\n",
        "\n",
        "BODY END
" @@ -650,11 +666,50 @@ "name": "stdout", "output_type": "stream", "text": [ - "TextContent(text=[{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 2 - Choose a Linux distribution\\nThe different versions of Linux are called \\u0022distributions\\u0022 (sometimes shortened to \\u201Cdistros\\u201D). So there isn\\u2019t actually a way to just \\u201Cinstall Linux\\u201D, you first need to choose which distribution that uses the Linux kernel you want to download and install. There are over 600 active Linux distributions, such as Ubuntu, Debian, Kali Linux, openSUSE, and more. There are a number of factors to consider in choosing a distribution, such as:\\n### Required Linux experience\\nIf you\\u2019re new to Linux, you may want to start with Ubuntu. This is one of the most common starting places for Linux users due to the level of support and ease of use. Ubuntu offers a desktop-version and a server-version depending on how you plan to use it. The specific tasks that the operating system will be responsible for may help you determine which distribution to use. Arch Linux is a popular choice for those who want a highly customizable, do-it-yourself approach that is still stable and well-supported by a large user base. It is a much more complicated place to start, but can help you to get a better understanding of how Linux works due to the amount of custom configuration.\\n### System requirements\\nA minimal, or \\u0022lightweight\\u0022, distribution means a small footprint, with lower memory and processing speed requirements. Alpine Linux is a popular option where users will find most features disabled or not installed by default, but still has everything you need for a containerized app. Security needs: Some Linux users may have specific security concerns to consider. Kali Linux is an example of a distro designed for security tasks, such as Penetration Testing, Security Research, Computer Forensics and Reverse Engineering.\\n### Business and Enterprise requirements\\nBusiness, corporate, or academic environments may have unique concerns from consumer environments. For example Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES) and Oracle Linux offer 24/7 global support and subscription-based service. CentOS is another popular enterprise distro that is community-supported but still compatible with RHEL.\\n### Community support\",\"contentUrl\":\"https://learn.microsoft.com/en-us/linux/install#step-2---choose-a-linux-distribution\"},{\"title\":\"New to Linux resources for SQL users\",\"content\":\"# New to Linux resources for SQL users\\n**Applies to:**[SQL Server](https://learn.microsoft.com/en-us/sql/sql-server/sql-docs-navigation-guide?view=sql-server-ver17#applies-to) - Linux\\nThis article is intended to provide a learning path through an introduction of Linux concepts. Use the sections in the article as a guided learning path to familiarize yourself with the Linux environment.\\nThis isn\\u0027t intended to be a comprehensive list but instead provides the bare minimum you need to help you administer and navigate your SQL Server on Linux environment. See the [full list of tutorials](https://www.linux.org/forums/linux-beginner-tutorials.123/) to learn more.\\n## What is Linux?\\nThe [What is Linux](https://www.linux.org/threads/what-is-linux.4106/) module introduces the history of Linux as an operating system. The module explains *the Kernel* and where Linux is today. This tutorial serves as an introduction to Linux, helping you get started.\\n## Select a distribution\\nAfter you know the history of Linux, decide which [distribution of Linux](https://www.linux.org/threads/selecting-a-linux-distribution.4117/) best suits your business needs. [SQL Server is supported](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-release-notes-2019?view=sql-server-ver17#supported-platforms) on various distributions such as Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES), and Ubuntu.\\n## Get around directories\\nAfter you have chosen the appropriate distribution of Linux, familiarize yourself with [getting around Linux directories](https://www.linux.org/threads/getting-around-in-linux-directories.4120/).\\nThis module will help you answer the following questions:\\n1. How to navigate between different files\\n2. How to identify a file from a directory\\n3. Identify different directories\\n## Install new software\\nNext, learn how to [install new software](https://www.linux.org/threads/installing-new-software-debian-red-hat-slackware.4119/) on your new Linux operating system. This module details installing new software to the Debian and Red Hat Linux operating systems.\",\"contentUrl\":\"https://learn.microsoft.com/en-us/sql/linux/new-to-linux-resources?view=sql-server-ver17\"},{\"title\":\"Part 1.2 - Basic operations on Linux\",\"content\":\"# Part 1.2 - Basic operations on Linux\\n## Run commands by using the \\u0022sudo\\u0022 prefix\\nInstead of becoming the root account user in your session, you can run commands as root by using the \\u0060sudo \\u003Ccommand\\u003E\\u0060 format. This approach is safer because it will run the given command only as a superuser.\\nFor example, if you run \\u0060restart\\u0060 while you\\u0027re not elevated to superuser status, the result is as follows.\\n![Screenshot of restart command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/restart-command.png)\\nTo run this command as a superuser instead, you can either change the session context to root (by running \\u0060sudo su\\u0060) or add the \\u0060sudo\\u0060 prefix, as follows:\\n\\u0060\\u0060\\u0060bash\\nsudo reboot\\n\\u0060\\u0060\\u0060\\nNow, the restart operation runs as expected by the virtual machine.\\n![Screenshot of reboot command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/reboot-command.png)\\n## Package managers\\nPackage managers are used to install, upgrade, and remove applications in Linux. There are several different package managers, such as DPKG (Debian Package Management System), RPM (Red Hat Package Manager), and Zypper for SUSE Package managers themselves are low-level. Therefore, it might be difficult to manage software directly by using the package manager. Fortunately, there are wrappers for these tools that make it easier to install and uninstall software. For example, [yum](https://wikipedia.org/wiki/Yum_%28software%29) is a wrapper around RPM (Red Hat Package Manager) and [APT](https://wikipedia.org/wiki/APT_%28software%29) is a wrapper around DPKG (Debian Package Management System).\\nThis tutorial assumes that you\\u0027re running the [Ubuntu package management system](https://ubuntu.com/server/docs/package-management). This system is derived from the same system that\\u0027s used by the Debian GNU/Linux distribution. Therefore, we can use APT to install the software.\\nDepending on your Linux choice, you might have to use other tools. For example, you can use yum to install or uninstall software if you\\u0027re running Red Hat.\",\"contentUrl\":\"https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-by-using-the-sudo-prefix\"},{\"title\":\"Part 1.2 - Basic operations on Linux\",\"content\":\"# Part 1.2 - Basic operations on Linux\\n## Run commands as a superuser\\nThe superuser is the most privileged user account. It has root (unrestricted) access to all files and folders, and complete control over the operation of the computer. The system administrator uses this account for system maintenance.\\nOn UNIX-like systems, the conventional name of the superuser is \\u0022root\\u0022. The root user can do tasks that are otherwise restricted for standard users, including the following ones:\\n1. Changing system directories\\n2. Changing user privileges\\n3. Changing file ownership\\n4. System-related tasks such as restarts\\n5. Installation of certain applications\\nThere are several ways to run commands as the root account user. One is to use the \\u0060sudo su\\u0060 command. In this command, \\u0060sudo\\u0060 is short for \\u0060superuser do\\u0060, and \\u0060su\\u0060 is an abbreviation for \\u0022switch user.\\u0022 You can also use the \\u0060su\\u0060 command to switch between standard users.\\nImportant\\nAfter you become the root account user, everything you do runs in root context. Therefore, you should act carefully because the root account has access to the entire system.\\nTo demonstrate a permissions issue, create a text file in a special folder that\\u0027s named */etc*.\\nThe */etc/* folder is where system configuration files are located. This folder protects its contents from being changed by a standard user.\\nRun the following command:\\n\\u0060\\u0060\\u0060bash\\necho hello world \\u003E /etc/helloworld.txt\\n\\u0060\\u0060\\u0060\\nThe \\u0060echo\\u0060 command writes all the text that follows it to the output. The angle bracket (\\u0060\\u003E\\u0060) tells the system to send the output to the */etc/helloworld.txt* file instead of the console. This behavior is similar to how Windows works.\\nNote\\nLinux is case-sensitive: *Helloworld.txt*, *helloworld.txt* and *helloworld.Txt* are different files.\\nYou don\\u0027t have to provide a file name extension. The name \\u0022helloworld\\u0022 by itself is perfectly valid. (The \\u0022txt\\u0022 extension is used here only as an example.)\\nWhen you run this command, you receive a **permission denied** error message.\\n![Screenshot of permission denied message.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/permission-denied-message.png)\\nThe operation fails because the */etc* folder is special folder that a standard user can\\u0027t change. Verify the user by running the \\u0060id\\u0060 command.\\n![Screenshot of I D command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/id-command.png)\",\"contentUrl\":\"https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 1 - Choose a method to install Linux\\nThe choice of which method to use for installing Linux depends on your needs and preferences.\\n1. **New to using Linux?** We recommend starting with Windows Subsystem for Linux (WSL) as it\\u0027s the easiest way to get up and running.\\n2. **Work in a business environment with more complex needs related to scale or security?** We recommend running Linux as a Virtual Machine (VM) in the cloud and checking out the support that Azure has to offer. This also applies if you want to run Linux as a server.\\n3. **Only want to run Linux as your primary operating system?** If you are good with a slightly more complicated install process and don\\u0027t need access to Windows tools (like Outlook, Teams, Word, PowerPoint, etc), you can run Linux on bare metal to access the full potential of your hardware without any overhead from virtualization or emulation.\\nLearn more about these options below.\\n### Windows Subsystem for Linux (WSL)\\nAvailable to those with a Windows machine, this is the most simple way to install Linux. Just run the Linux install command: \\u0060wsl --install\\u0060 to install the Ubuntu distribution.\\nThis method uses virtualization to integrate the Windows operating system (OS) with the Linux OS (running on an actual Linux kernel). You can add as many different Linux distributions as you please and run your favorite Linux tools, including GUI apps, alongside your favorite Windows tools. You can also mix and match Bash and PowerShell commands in the same command line. A Linux file system hosts your project files, while a separate Windows file systems hosts any developer tools, such as VS Code, that you\\u0027d like to work on the files with. This method of running Linux is highly efficient and performant. Once you choose a Linux distribution or opt to use the default Ubuntu distro, follow the instructions to Use the install Linux command with Windows Subsystem for Linux or learn more in the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl).\\n![WSL install command screenshot](https://learn.microsoft.com/en-us/linux/images/wsl-install-command-screenshot.png)\\n### Create a Virtual Machine (VM)\\nAvailable to everyone regardless if you\\u0027re using a Windows or MacOS-based machine, but will require either an Azure account, if creating a VM in the cloud, or virtualization software, if creating a VM directly on your machine (locally).\",\"contentUrl\":\"https://learn.microsoft.com/en-us/linux/install#step-1---choose-a-method-to-install-linux\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\nLinux is an operating system, similar to Windows, but with many different versions due to the nature of being open source and fully customizable. To install Linux, you must choose an install method and choose a Linux distribution.\\nTo install Linux:\\n1. Choose an install method: Windows Subsystem for Linux (WSL), Bare metal Linux; or create a Virtual Machine (VM) to run Linux locally or in the cloud.\\n2. Choose a Linux distribution: Ubuntu, Debian, Kali Linux, openSUSE, etc.\\n3. Follow the steps for your preferred install method:\\n3.1. Use the install Linux command with Windows Subsystem for Linux (WSL)\\n3.2. Create a Linux Virtual Machine (VM) in the cloud\\n3.3. Create a Linux Virtual Machine (VM) on your local machine\\n3.4. Create a bootable USB to install bare-metal Linux\\n4. After installing Linux: Get familiar with your distribution\\u0027s package manager, update and upgrade the packages available, and get familiar with the other [Linux resources at Microsoft](https://learn.microsoft.com/en-us/linux/), such as training courses, Linux-versions of popular tools, news, and Open Source events.\",\"contentUrl\":\"https://learn.microsoft.com/en-us/linux/install\"},{\"title\":\"Guide for changing your dev environment from Mac to Windows\",\"content\":\"# Guide for changing your dev environment from Mac to Windows\\n## Command-line shells and terminals\\nYou can now install Windows Subsystem for Linux (WSL) to support running a Linux shell within Windows. This means that you can run **bash**, with whichever specific Linux distribution you choose, integrated right inside Windows. Using WSL provides the kind of environment most familiar to Mac users. For example, you use **ls** to list the files in a current directory, not **dir** as you would with the traditional Windows Cmd Shell. To learn about installing and using WSL, see the [Windows Subsystem for Linux Installation Guide](https://learn.microsoft.com/en-us/windows/wsl/install). Linux distributions that you can install on Windows with WSL include:\\n1. [Ubuntu 20.04 LTS](https://www.microsoft.com/store/apps/9n6svws3rx71)\\n2. [Kali Linux](https://www.microsoft.com/store/apps/9PKR34TNCV07)\\n3. [Debian GNU/Linux](https://www.microsoft.com/store/apps/9MSVKQC78PK6)\\n4. [openSUSE Leap 15.1](https://www.microsoft.com/store/apps/9NJFZK00FGKV)\\n5. [SUSE Linux Enterprise Server 15 SP1](https://www.microsoft.com/store/apps/9PN498VPMF3Z)\\nJust to name a few. Find more in the [WSL install docs](https://learn.microsoft.com/en-us/windows/wsl/install-win10#install-your-linux-distribution-of-choice) and install them directly from the [Microsoft Store](https://aka.ms/wslstore).\",\"contentUrl\":\"https://learn.microsoft.com/en-us/windows/dev-environment/mac-to-windows#command-line-shells-and-terminals\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 3 - Follow install method instructions\\n### Install Linux with Windows Subsystem for Linux\\nTo install Linux on a Windows PC, use the install Linux command.\\n1. Open PowerShell or Windows Command Prompt in *administrator mode* by right-clicking and selecting \\u0022Run as administrator\\u0022.\\n2. Enter the Linux install command: \\u0060wsl --install\\u0060.\\n3. Restart your machine.\\n![WSL Linux Install List Command screenshot](https://learn.microsoft.com/en-us/linux/images/wsl-install-list-screenshot.png)\\n#### Install Linux command options\\nBy default, the install command will use the Ubuntu distribution of Linux. You can see the other distributions that are available in the Microsoft Store by entering the command: \\u0060wsl -l -o\\u0060. If you\\u0027d like to install a distribution other than Ubuntu, just add \\u0060-d\\u0060 to the command with the name of the distribution you\\u0027d like to install:\\n\\u0060\\u0060\\u0060powershell\\nwsl --install -d \\u003CDistro Name\\u003E\\n\\u0060\\u0060\\u0060\\nIf the distribution you want to install isn\\u0027t available in the Microsoft Store, see the Windows Subsystem for Linux (WSL) documentation on how to [Import any Linux distribution to use with WSL](https://learn.microsoft.com/en-us/windows/wsl/use-custom-distro).\\nFor more information on how to use VS Code, Git, databases, Docker, GUI apps, GPU acceleration, NodesJS, USB devices, or mount a disk, build a custom distribution, manage disk space, or set up WSL for your enterprise business, see the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl/).\\n### Create a Linux Virtual Machine in the cloud\\nTo create a Linux Virtual Machine (VM) that is hosted in the cloud on Azure, log-in to the Azure Portal and select the \\u201CCreate a Virtual Machine\\u201D service. Then follow the instructions for creating a user name, project, instance, SSH key, ports, and copying the IP address.\\nAzure supports a variety of options for setting up Linux VMs in the cloud. For a quickstart guide on how to set up a Linux VM, choose your preferred method:\\n1. [Azure Portal](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-portal?tabs=ubuntu\\u0026source=docs)\\n2. [Azure CLI](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-cli)\\n3. [Azure PowerShell](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-powershell)\\n4. [Terraform](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-terraform)\",\"contentUrl\":\"https://learn.microsoft.com/en-us/linux/install#step-3---follow-install-method-instructions\"},{\"title\":\"How to download and install Linux\",\"content\":\"# How to download and install Linux\\n## Step 2 - Choose a Linux distribution\\nA large user community that consistently contributes to the distribution, fixing bugs, adding features, helping with questions, etc. is another good consideration. Ubuntu is one of the most popular distos. openSUSE has been around longer than most other distributions and offers in addition to the stable version (Leap) a rolling version (Tumbleweed). Many other distros rapidly grow popular as the needs and preferences of the user community change. The Linux.org site has resources to help you assess what\\u0027s available, well-supported, or growing in popularity.\\n![Screenshot of the list of distributions on Linux.org](https://learn.microsoft.com/en-us/linux/images/linux-distributions-list-screenshot.png)\",\"contentUrl\":\"https://learn.microsoft.com/en-us/linux/install#step-2---choose-a-linux-distribution\"},{\"title\":\"Part 1.2 - Basic operations on Linux\",\"content\":\"# Part 1.2 - Basic operations on Linux\\n## Run commands as a superuser\\nTo become the root account user, run the \\u0060sudo su\\u0060 command.\\n![Screenshot of sudo su command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/sudo-su-command.png)\\nYou should notice two things: When you become root, the dollar sign character (\\u0060$\\u0060) becomes a pound sign character (\\u0060#\\u0060). Also, the username is changed to **root**. If you run the same \\u0060id\\u0060 command again, you see that the user and group ID of root are **0**.\\n![Screenshot of root command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/root-command.png)\\nImportant\\nNow you have elevated yourself to the \\u0022superuser\\u0022 role in your session, you have complete access to the system. Keep in mind that this is a potentially dangerous situation, and you should exercise caution as you continue.\\nNow, run the same command one more time:\\n\\u0060\\u0060\\u0060bash\\necho hello world \\u003E /etc/helloworld.txt\\n\\u0060\\u0060\\u0060\\nThis time, you don\\u0027t receive any error message. To verify if the file is created, run the following command:\\n\\u0060\\u0060\\u0060bash\\nll /etc/hello*\\n\\u0060\\u0060\\u0060\\nThe \\u0060ll\\u0060 command lists the files and subfolders that are in a specified folder. In this example, the \\u0060/etc/hello*\\u0060 parameter value limits the command output to the files or folders whose name begins in \\u0060hello\\u0060.\\n![Screenshot of hello command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/hello-command.png)\\nThe output shows that the file was created. What if we want to examine the content of the file? The \\u0060cat\\u0060 command helps here. To see the content, run \\u0060cat /etc/helloworld.txt\\u0060.\\n![Screenshot of cat command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/cat-command.png)\\nThe \\u0060cat\\u0060 command reads files sequentially, and writes their content to the standard output. Therefore, it writes \\u0022hello world\\u0022 in the console.\\nTo prevent mistakes that could damage the system while you\\u0027re elevated to root status, it\\u0027s a good practice to exit the root session and return to your standard user session to avoid some dangerous operations. To do this, run \\u0060exit\\u0060. You can see that the pound sign reverts to a dollar sign, and the username is displayed as your standard user.\\n![Screenshot of exit command.](https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/exit-command.png)\\nFor additional practice, run the same \\u0060ll\\u0060 and \\u0060cat\\u0060 commands within your standard user context. You\\u0027ll see that you can\\u0027t create the file in the */etc/* folder, but can you list the file and read the contents.\",\"contentUrl\":\"https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\"}], annotations=null, meta=null)\n" + "Title: \"How to download and install Linux\"\n", + "\n", + "Content: \"# How to download and install Linux\\n## Step 2 - Choose a Linux distribution\\nThe different versions of Linux are called \\\"distributions\\\" (sometimes shortened to “distros”). So there isn’t actually a way to just “install Linux”, you first need to choose which distribution that uses the Linux kernel you want to download and install. There are over 600 active Linux distributions, such as Ubuntu, Debian, Kali Linux, openSUSE, and more. There are a number of factors to consider in choosing a distribution, such as:\\n### Required Linux experience\\nIf you’re new to Linux, you may want to start with Ubuntu. This is one of the most common starting places for Linux users due to the level of support and ease of use. Ubuntu offers a desktop-version and a server-version depending on how you plan to use it. The specific tasks that the operating system will be responsible for may help you determine which distribution to use. Arch Linux is a popular choice for those who want a highly customizable, do-it-yourself approach that is still stable and well-supported by a large user base. It is a much more complicated place to start, but can help you to get a better understanding of how Linux works due to the amount of custom configuration.\\n### System requirements\\nA minimal, or \\\"lightweight\\\", distribution means a small footprint, with lower memory and processing speed requirements. Alpine Linux is a popular option where users will find most features disabled or not installed by default, but still has everything you need for a containerized app. Security needs: Some Linux users may have specific security concerns to consider. Kali Linux is an example of a distro designed for security tasks, such as Penetration Testing, Security Research, Computer Forensics and Reverse Engineering.\\n### Business and Enterprise requirements\\nBusiness, corporate, or academic environments may have unique concerns from consumer environments. For example Red Hat Enterprise Linux (RHEL), SUSE Linux Enterprise Server (SLES) and Oracle Linux offer 24/7 global support and subscription-based service. CentOS is another popular enterprise distro that is community-supported but still compatible with RHEL.\\n### Community support\"\n", + "Content URL: \"https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\"\n", + "Title: \"Part 1.2 - Basic operations on Linux\"\n", + "\n", + "Content: \"# Part 1.2 - Basic operations on Linux\\n## Run commands by using the \\\"sudo\\\" prefix\\nInstead of becoming the root account user in your session, you can run commands as root by using the `sudo ` format. This approach is safer because it will run the given command only as a superuser.\\nFor example, if you run `restart` while you're not elevated to superuser status, the result is as follows.\\n![Screenshot of restart command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/restart-command.png)\\nTo run this command as a superuser instead, you can either change the session context to root (by running `sudo su`) or add the `sudo` prefix, as follows:\\n```bash\\nsudo reboot\\n```\\nNow, the restart operation runs as expected by the virtual machine.\\n![Screenshot of reboot command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/reboot-command.png)\\n## Package managers\\nPackage managers are used to install, upgrade, and remove applications in Linux. There are several different package managers, such as DPKG (Debian Package Management System), RPM (Red Hat Package Manager), and Zypper for SUSE Package managers themselves are low-level. Therefore, it might be difficult to manage software directly by using the package manager. Fortunately, there are wrappers for these tools that make it easier to install and uninstall software. For example, [yum](https://wikipedia.org/wiki/Yum_%28software%29) is a wrapper around RPM (Red Hat Package Manager) and [APT](https://wikipedia.org/wiki/APT_%28software%29) is a wrapper around DPKG (Debian Package Management System).\\nThis tutorial assumes that you're running the [Ubuntu package management system](https://ubuntu.com/server/docs/package-management). This system is derived from the same system that's used by the Debian GNU/Linux distribution. Therefore, we can use APT to install the software.\\nDepending on your Linux choice, you might have to use other tools. For example, you can use yum to install or uninstall software if you're running Red Hat.\"\n", + "Content URL: \"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-by-using-the-sudo-prefix\"\n", + "Title: \"Part 1.2 - Basic operations on Linux\"\n", + "\n", + "Content: \"# Part 1.2 - Basic operations on Linux\\n## Run commands as a superuser\\nThe superuser is the most privileged user account. It has root (unrestricted) access to all files and folders, and complete control over the operation of the computer. The system administrator uses this account for system maintenance.\\nOn UNIX-like systems, the conventional name of the superuser is \\\"root\\\". The root user can do tasks that are otherwise restricted for standard users, including the following ones:\\n1. Changing system directories\\n2. Changing user privileges\\n3. Changing file ownership\\n4. System-related tasks such as restarts\\n5. Installation of certain applications\\nThere are several ways to run commands as the root account user. One is to use the `sudo su` command. In this command, `sudo` is short for `superuser do`, and `su` is an abbreviation for \\\"switch user.\\\" You can also use the `su` command to switch between standard users.\\nImportant\\nAfter you become the root account user, everything you do runs in root context. Therefore, you should act carefully because the root account has access to the entire system.\\nTo demonstrate a permissions issue, create a text file in a special folder that's named */etc*.\\nThe */etc/* folder is where system configuration files are located. This folder protects its contents from being changed by a standard user.\\nRun the following command:\\n```bash\\necho hello world > /etc/helloworld.txt\\n```\\nThe `echo` command writes all the text that follows it to the output. The angle bracket (`>`) tells the system to send the output to the */etc/helloworld.txt* file instead of the console. This behavior is similar to how Windows works.\\nNote\\nLinux is case-sensitive: *Helloworld.txt*, *helloworld.txt* and *helloworld.Txt* are different files.\\nYou don't have to provide a file name extension. The name \\\"helloworld\\\" by itself is perfectly valid. (The \\\"txt\\\" extension is used here only as an example.)\\nWhen you run this command, you receive a **permission denied** error message.\\n![Screenshot of permission denied message.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/permission-denied-message.png)\\nThe operation fails because the */etc* folder is special folder that a standard user can't change. Verify the user by running the `id` command.\\n![Screenshot of I D command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/id-command.png)\"\n", + "Content URL: \"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\"\n", + "Title: \"How to download and install Linux\"\n", + "\n", + "Content: \"# How to download and install Linux\\n## Step 1 - Choose a method to install Linux\\nThe choice of which method to use for installing Linux depends on your needs and preferences.\\n1. **New to using Linux?** We recommend starting with Windows Subsystem for Linux (WSL) as it's the easiest way to get up and running.\\n2. **Work in a business environment with more complex needs related to scale or security?** We recommend running Linux as a Virtual Machine (VM) in the cloud and checking out the support that Azure has to offer. This also applies if you want to run Linux as a server.\\n3. **Only want to run Linux as your primary operating system?** If you are good with a slightly more complicated install process and don't need access to Windows tools (like Outlook, Teams, Word, PowerPoint, etc), you can run Linux on bare metal to access the full potential of your hardware without any overhead from virtualization or emulation.\\nLearn more about these options below.\\n### Windows Subsystem for Linux (WSL)\\nAvailable to those with a Windows machine, this is the most simple way to install Linux. Just run the Linux install command: `wsl --install` to install the Ubuntu distribution.\\nThis method uses virtualization to integrate the Windows operating system (OS) with the Linux OS (running on an actual Linux kernel). You can add as many different Linux distributions as you please and run your favorite Linux tools, including GUI apps, alongside your favorite Windows tools. You can also mix and match Bash and PowerShell commands in the same command line. A Linux file system hosts your project files, while a separate Windows file systems hosts any developer tools, such as VS Code, that you'd like to work on the files with. This method of running Linux is highly efficient and performant. Once you choose a Linux distribution or opt to use the default Ubuntu distro, follow the instructions to Use the install Linux command with Windows Subsystem for Linux or learn more in the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl).\\n![WSL install command screenshot](https://learn.microsoft.com/linux/images/wsl-install-command-screenshot.png)\\n### Create a Virtual Machine (VM)\\nAvailable to everyone regardless if you're using a Windows or MacOS-based machine, but will require either an Azure account, if creating a VM in the cloud, or virtualization software, if creating a VM directly on your machine (locally).\"\n", + "Content URL: \"https://learn.microsoft.com/linux/install#step-1---choose-a-method-to-install-linux\"\n", + "Title: \"How to download and install Linux\"\n", + "\n", + "Content: \"# How to download and install Linux\\nLinux is an operating system, similar to Windows, but with many different versions due to the nature of being open source and fully customizable. To install Linux, you must choose an install method and choose a Linux distribution.\\nTo install Linux:\\n1. Choose an install method: Windows Subsystem for Linux (WSL), Bare metal Linux; or create a Virtual Machine (VM) to run Linux locally or in the cloud.\\n2. Choose a Linux distribution: Ubuntu, Debian, Kali Linux, openSUSE, etc.\\n3. Follow the steps for your preferred install method:\\n3.1. Use the install Linux command with Windows Subsystem for Linux (WSL)\\n3.2. Create a Linux Virtual Machine (VM) in the cloud\\n3.3. Create a Linux Virtual Machine (VM) on your local machine\\n3.4. Create a bootable USB to install bare-metal Linux\\n4. After installing Linux: Get familiar with your distribution's package manager, update and upgrade the packages available, and get familiar with the other [Linux resources at Microsoft](https://learn.microsoft.com/linux/), such as training courses, Linux-versions of popular tools, news, and Open Source events.\"\n", + "Content URL: \"https://learn.microsoft.com/linux/install\"\n", + "Title: \"New to Linux resources for SQL users\"\n", + "\n", + "Content: \"# New to Linux resources for SQL users\\n**Applies to:**![](https://learn.microsoft.com/sql/includes/media/yes-icon.svg?view=sql-server-ver17)[SQL Server](https://learn.microsoft.com/sql/sql-server/sql-docs-navigation-guide?view=sql-server-ver17#applies-to) on Linux\\nThis article provides a learning path through an introduction to Linux concepts. Use the sections in the article as a guided learning path to familiarize yourself with the Linux environment.\\nThis list isn't comprehensive but instead provides the bare minimum you need to help you administer and navigate your SQL Server on Linux environment. See the [full list of tutorials](https://www.linux.org/forums/linux-beginner-tutorials.123/) to learn more.\\n## What is Linux?\\nThe [What is Linux](https://www.linux.org/threads/what-is-linux.4106/) module introduces the history of Linux as an operating system. The module explains *the Kernel* and where Linux is today. This tutorial serves as an introduction to Linux, helping you get started.\\n## Select a distribution\\nAfter you know the history of Linux, decide which [distribution of Linux](https://www.linux.org/threads/selecting-a-linux-distribution.4117/) best suits your business needs.\\nSQL Server 2025 (17.x) is [supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2025?view=sql-server-ver17#supported-platforms) on Red Hat Enterprise Linux (RHEL) and Ubuntu.\\nSQL Server 2022 (16.x) and earlier versions are [also supported](https://learn.microsoft.com/sql/linux/sql-server-linux-release-notes-2022?view=sql-server-ver17#supported-platforms) on SUSE Linux Enterprise Server (SLES).\\n## Get around directories\\nAfter you choose the appropriate distribution of Linux, familiarize yourself with [getting around Linux directories](https://www.linux.org/threads/getting-around-in-linux-directories.4120/).\\nThis module helps you answer the following questions:\\n1. How to navigate between different files\\n2. How to identify a file from a directory\\n3. Identify different directories\\n## Install new software\\nNext, learn how to [install new software](https://www.linux.org/threads/installing-new-software-debian-red-hat-slackware.4119/) on your new Linux operating system. This module covers installing new software on the Debian and Red Hat Linux operating systems.\"\n", + "Content URL: \"https://learn.microsoft.com/sql/linux/new-to-linux-resources?view=sql-server-ver17\"\n", + "Title: \"How to download and install Linux\"\n", + "\n", + "Content: \"# How to download and install Linux\\n## Step 3 - Follow install method instructions\\n### Install Linux with Windows Subsystem for Linux\\nTo install Linux on a Windows PC, use the install Linux command.\\n1. Open PowerShell or Windows Command Prompt in *administrator mode* by right-clicking and selecting \\\"Run as administrator\\\".\\n2. Enter the Linux install command: `wsl --install`.\\n3. Restart your machine.\\n![WSL Linux Install List Command screenshot](https://learn.microsoft.com/linux/images/wsl-install-list-screenshot.png)\\n#### Install Linux command options\\nBy default, the install command will use the Ubuntu distribution of Linux. You can see the other distributions that are available in the Microsoft Store by entering the command: `wsl -l -o`. If you'd like to install a distribution other than Ubuntu, just add `-d` to the command with the name of the distribution you'd like to install:\\n```powershell\\nwsl --install -d \\n```\\nIf the distribution you want to install isn't available in the Microsoft Store, see the Windows Subsystem for Linux (WSL) documentation on how to [Import any Linux distribution to use with WSL](https://learn.microsoft.com/en-us/windows/wsl/use-custom-distro).\\nFor more information on how to use VS Code, Git, databases, Docker, GUI apps, GPU acceleration, NodesJS, USB devices, or mount a disk, build a custom distribution, manage disk space, or set up WSL for your enterprise business, see the [WSL documentation](https://learn.microsoft.com/en-us/windows/wsl/).\\n### Create a Linux Virtual Machine in the cloud\\nTo create a Linux Virtual Machine (VM) that is hosted in the cloud on Azure, log-in to the Azure Portal and select the “Create a Virtual Machine” service. Then follow the instructions for creating a user name, project, instance, SSH key, ports, and copying the IP address.\\nAzure supports a variety of options for setting up Linux VMs in the cloud. For a quickstart guide on how to set up a Linux VM, choose your preferred method:\\n1. [Azure Portal](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-portal?tabs=ubuntu&source=docs)\\n2. [Azure CLI](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-cli)\\n3. [Azure PowerShell](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-powershell)\\n4. [Terraform](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/quick-create-terraform)\"\n", + "Content URL: \"https://learn.microsoft.com/linux/install#step-3---follow-install-method-instructions\"\n", + "Title: \"How to download and install Linux\"\n", + "\n", + "Content: \"# How to download and install Linux\\n## Step 2 - Choose a Linux distribution\\nA large user community that consistently contributes to the distribution, fixing bugs, adding features, helping with questions, etc. is another good consideration. Ubuntu is one of the most popular distos. openSUSE has been around longer than most other distributions and offers in addition to the stable version (Leap) a rolling version (Tumbleweed). Many other distros rapidly grow popular as the needs and preferences of the user community change. The Linux.org site has resources to help you assess what's available, well-supported, or growing in popularity.\\n![Screenshot of the list of distributions on Linux.org](https://learn.microsoft.com/linux/images/linux-distributions-list-screenshot.png)\"\n", + "Content URL: \"https://learn.microsoft.com/linux/install#step-2---choose-a-linux-distribution\"\n", + "Title: \"Part 1.2 - Basic operations on Linux\"\n", + "\n", + "Content: \"# Part 1.2 - Basic operations on Linux\\n## Run commands as a superuser\\nTo become the root account user, run the `sudo su` command.\\n![Screenshot of sudo su command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/sudo-su-command.png)\\nYou should notice two things: When you become root, the dollar sign character (`$`) becomes a pound sign character (`#`). Also, the username is changed to **root**. If you run the same `id` command again, you see that the user and group ID of root are **0**.\\n![Screenshot of root command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/root-command.png)\\nImportant\\nNow you have elevated yourself to the \\\"superuser\\\" role in your session, you have complete access to the system. Keep in mind that this is a potentially dangerous situation, and you should exercise caution as you continue.\\nNow, run the same command one more time:\\n```bash\\necho hello world > /etc/helloworld.txt\\n```\\nThis time, you don't receive any error message. To verify if the file is created, run the following command:\\n```bash\\nll /etc/hello*\\n```\\nThe `ll` command lists the files and subfolders that are in a specified folder. In this example, the `/etc/hello*` parameter value limits the command output to the files or folders whose name begins in `hello`.\\n![Screenshot of hello command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/hello-command.png)\\nThe output shows that the file was created. What if we want to examine the content of the file? The `cat` command helps here. To see the content, run `cat /etc/helloworld.txt`.\\n![Screenshot of cat command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/cat-command.png)\\nThe `cat` command reads files sequentially, and writes their content to the standard output. Therefore, it writes \\\"hello world\\\" in the console.\\nTo prevent mistakes that could damage the system while you're elevated to root status, it's a good practice to exit the root session and return to your standard user session to avoid some dangerous operations. To do this, run `exit`. You can see that the pound sign reverts to a dollar sign, and the username is displayed as your standard user.\\n![Screenshot of exit command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/exit-command.png)\\nFor additional practice, run the same `ll` and `cat` commands within your standard user context. You'll see that you can't create the file in the */etc/* folder, but can you list the file and read the contents.\"\n", + "Content URL: \"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#run-commands-as-a-superuser\"\n", + "Title: \"Part 1.2 - Basic operations on Linux\"\n", + "\n", + "Content: \"# Part 1.2 - Basic operations on Linux\\n## Remove packages\\nFinding the file doesn't involve package managers. Instead, you'll run a standard search for files in Linux. There are several ways to do so. One of the most common search commands is `find`. However, this command is also confusing to use, so we won't discuss it here. Instead, we'll use `whereis`.\\nNote\\nLinux is very well documented. Nearly every command has a useful help page available. Use the [man](https://www.geeksforgeeks.org/man-command-in-linux-with-examples/) and [info](https://www.geeksforgeeks.org/info-command-in-linux-with-examples/) commands to show the Help pages in Linux. For example, to learn more on your own about the `find` command, run `man find` or `info find`.\\nAccording to the Help page for `whereis`, the definition is as follows:\\n`whereis` locates the binary, source, and manual files for the specified command names.\\nIf you run `whereis apache2`, you should find the */etc/apache2* installation.\\n![Screenshot of whereis apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/whereis-apache2-command.png)\\nRemember that the */etc/* directory is where the system configuration files are located. To learn whether \\\"apache2\\\"is a file or folder, run `ll /etc/apache2`. As you can see, this is a folder, and it contains the apache2 configuration files.\\n![Screenshot of ll etc apache2 command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/ll-etc-apache2-command.png)\\nYou expect `apache2` to contain these files and folders because you used the `apt remove` command that doesn't delete the configuration files.\\nAs final step, run the `apt purge` command to see its effect. You can run a purge to clean up the installation folder even after you remove the package. The following screenshot shows that `whereis` didn't find anything. This is because the `purge` command removed the configuration files together with the binaries.\\n![Screenshot of apt purge command.](https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/media/1-2-linux-special-directories-users-package-managers/apt-purge-command.png)\"\n", + "Content URL: \"https://learn.microsoft.com/troubleshoot/developer/webapps/aspnetcore/practice-troubleshoot-linux/1-2-linux-special-directories-users-package-managers#remove-packages\"\n" ] } ], - "execution_count": 16 + "execution_count": 14 } ], "metadata": { diff --git a/samples/simple-streamable-server/build.gradle.kts b/samples/simple-streamable-server/build.gradle.kts index 8cb908e1c..02964296f 100644 --- a/samples/simple-streamable-server/build.gradle.kts +++ b/samples/simple-streamable-server/build.gradle.kts @@ -22,6 +22,15 @@ dependencies { implementation(libs.ktor.server.sse) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.slf4j.simple) + + testImplementation(libs.mcp.kotlin.client) + testImplementation(libs.ktor.client.cio) + testImplementation(libs.junit.jupiter) + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() } kotlin { diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index 1f893bd67..a9fced704 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -1,7 +1,8 @@ [versions] kotlin = "2.2.21" ktor = "3.2.3" -mcp-kotlin = "0.9.0" +junit = "6.0.3" +mcp-kotlin = "0.10.0" slf4j = "2.0.17" shadow = "9.4.1" @@ -13,7 +14,10 @@ ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-conte ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json" } ktor-server-auth = { group = "io.ktor", name = "ktor-server-auth" } ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse" } +mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } +ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } +junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } [plugins] diff --git a/samples/simple-streamable-server/src/test/kotlin/io/modelcontextprotocol/sample/server/ServerIntegrationTest.kt b/samples/simple-streamable-server/src/test/kotlin/io/modelcontextprotocol/sample/server/ServerIntegrationTest.kt new file mode 100644 index 000000000..c4dae4685 --- /dev/null +++ b/samples/simple-streamable-server/src/test/kotlin/io/modelcontextprotocol/sample/server/ServerIntegrationTest.kt @@ -0,0 +1,146 @@ +package io.modelcontextprotocol.sample.server + +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.sse.SSE +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ServerIntegrationTest { + + private val server = embeddedServer(Netty, host = "127.0.0.1", port = 0) { + configureServer() + } + + private val httpClient = HttpClient(CIO) { + install(SSE) + } + + private lateinit var mcpClient: Client + + @BeforeAll + suspend fun beforeAll() { + server.start(wait = false) + val port = server.engine.resolvedConnectors().single().port + + mcpClient = httpClient.mcpStreamableHttp( + url = "http://127.0.0.1:$port/mcp", + ) + } + + @AfterAll + suspend fun afterAll() { + mcpClient.close() + httpClient.close() + server.stop(500, 1000) + } + + @Test + fun `should negotiate server capabilities`() { + val capabilities = mcpClient.serverCapabilities + assertNotNull(capabilities) + assertNotNull(capabilities.tools) + assertNotNull(capabilities.prompts) + assertNotNull(capabilities.resources) + assertNotNull(capabilities.logging) + } + + @Test + fun `should report server version`() { + val version = mcpClient.serverVersion + assertNotNull(version) + assertEquals("simple-streamable-http-server", version.name) + assertEquals("1.0.0", version.version) + } + + @Test + suspend fun `should list tools`() { + val result = mcpClient.listTools() + val toolNames = result.tools.map { it.name }.sorted() + assertEquals(listOf("greet", "multi-greet"), toolNames) + } + + @Test + suspend fun `should call greet tool`() { + val result = mcpClient.callTool(name = "greet", arguments = mapOf("name" to "Alice")) + val content = result.content.single() + assertIs(content) + assertEquals("Hello, Alice!", content.text) + } + + @Test + suspend fun `should call greet tool with default name`() { + val result = mcpClient.callTool(name = "greet", arguments = emptyMap()) + val content = result.content.single() + assertIs(content) + assertEquals("Hello, World!", content.text) + } + + @Test + suspend fun `should call multi-greet tool with logging notifications`() { + val result = mcpClient.callTool(name = "multi-greet", arguments = mapOf("name" to "Bob")) + val content = result.content.single() + assertIs(content) + assertEquals("Good morning, Bob!", content.text) + } + + @Test + suspend fun `should list prompts`() { + val result = mcpClient.listPrompts() + assertEquals(listOf("greeting-template"), result.prompts.map { it.name }) + } + + @Test + suspend fun `should get greeting template prompt`() { + val result = mcpClient.getPrompt( + GetPromptRequest( + GetPromptRequestParams( + name = "greeting-template", + arguments = mapOf("name" to "Charlie"), + ), + ), + ) + val message = result.messages.single() + assertEquals(Role.User, message.role) + val content = message.content + assertIs(content) + assertEquals("Please greet Charlie in a friendly manner.", content.text) + } + + @Test + suspend fun `should list resources`() { + val result = mcpClient.listResources() + assertEquals(listOf("Default Greeting"), result.resources.map { it.name }) + } + + @Test + suspend fun `should read default greeting resource`() { + val result = mcpClient.readResource( + ReadResourceRequest( + ReadResourceRequestParams(uri = "https://example.com/greetings/default"), + ), + ) + val content = result.contents.single() + assertIs(content) + assertEquals("Hello, world!", content.text) + assertEquals("text/plain", content.mimeType) + } +} + diff --git a/samples/simple-streamable-server/src/test/resources/simplelogger.properties b/samples/simple-streamable-server/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..ad2d3de88 --- /dev/null +++ b/samples/simple-streamable-server/src/test/resources/simplelogger.properties @@ -0,0 +1,6 @@ +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.modelcontextprotocol.sample.server=DEBUG From fdecf6bdf55119438ae15fcfc1e33a6ea634ec85 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 1 Apr 2026 12:20:12 +0200 Subject: [PATCH 060/187] feat: add URL mode elicitation and typed schema definitions per MCP (#660) Add URL mode elicitation support and typed `PrimitiveSchemaDefinition` hierarchy to align with MCP specification 2025-11-25 closes #540 ## How Has This Been Tested? unit/integration/conformance ## Breaking Changes - `ElicitRequestParams` changed from `data class` to `sealed interface`. A deprecated factory function preserves source compatibility for `ElicitRequestParams(message, requestedSchema)`. - `RequestedSchema.properties` changed from `JsonObject` to `Map`. - `ElicitRequest.requestedSchema` is deprecated, use `(params as ElicitRequestFormParams).requestedSchema`. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- conformance-test/run-conformance.sh | 2 +- .../sdk/conformance/ConformanceTools.kt | 296 +++------ .../kotlin/sdk/client/ClientTest.kt | 18 +- kotlin-sdk-core/api/kotlin-sdk-core.api | 570 +++++++++++++++++- .../kotlin/sdk/types/elicitation.dsl.kt | 6 +- .../kotlin/sdk/types/elicitation.kt | 137 ++++- .../kotlin/sdk/types/schema.kt | 292 +++++++++ .../kotlin/sdk/types/serializers.kt | 78 +++ .../kotlin/sdk/types/ElicitationTest.kt | 226 ++++--- .../kotlin/sdk/types/PrimitiveSchemaTest.kt | 481 +++++++++++++++ .../sdk/types/dsl/ElicitationDslTest.kt | 19 +- kotlin-sdk-server/api/kotlin-sdk-server.api | 7 + .../kotlin/sdk/server/ClientConnection.kt | 48 +- .../kotlin/sdk/server/Server.kt | 24 + .../kotlin/sdk/server/ServerSession.kt | 18 + 15 files changed, 1838 insertions(+), 384 deletions(-) create mode 100644 kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt create mode 100644 kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt diff --git a/conformance-test/run-conformance.sh b/conformance-test/run-conformance.sh index 8f15ac0d1..279254ebb 100755 --- a/conformance-test/run-conformance.sh +++ b/conformance-test/run-conformance.sh @@ -9,7 +9,7 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1; pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." || exit 1; pwd)" -CONFORMANCE_VERSION="0.1.15" +CONFORMANCE_VERSION="0.1.16" PORT="${MCP_PORT:-3001}" SERVER_URL="http://localhost:${PORT}/mcp" RESULTS_DIR="$SCRIPT_DIR/results" diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt index 579b22232..54d5c8c44 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt @@ -3,12 +3,17 @@ package io.modelcontextprotocol.kotlin.sdk.conformance import io.github.oshai.kotlinlogging.KotlinLogging import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.types.AudioContent +import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams import io.modelcontextprotocol.kotlin.sdk.types.EmbeddedResource +import io.modelcontextprotocol.kotlin.sdk.types.EnumOption import io.modelcontextprotocol.kotlin.sdk.types.ImageContent +import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema +import io.modelcontextprotocol.kotlin.sdk.types.LegacyTitledEnumSchema import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams @@ -16,11 +21,15 @@ import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams import io.modelcontextprotocol.kotlin.sdk.types.Role import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.TitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.TitledSingleSelectEnumSchema import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema import kotlinx.coroutines.delay -import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.double @@ -187,22 +196,10 @@ fun Server.registerConformanceTools() { ) { request -> val message = request.arguments?.get("message")?.jsonPrimitive?.content ?: "Please provide input" val schema = ElicitRequestParams.RequestedSchema( - properties = buildJsonObject { - put( - "username", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("User's response")) - }, - ) - put( - "email", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("User's email address")) - }, - ) - }, + properties = mapOf( + "username" to StringSchema(description = "User's response"), + "email" to StringSchema(description = "User's email address"), + ), required = listOf("username", "email"), ) val result = createElicitation(message, schema) @@ -215,54 +212,17 @@ fun Server.registerConformanceTools() { description = "test_elicitation_sep1034_defaults", ) { val schema = ElicitRequestParams.RequestedSchema( - properties = buildJsonObject { - put( - "name", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("User name")) - put("default", JsonPrimitive("John Doe")) - }, - ) - put( - "age", - buildJsonObject { - put("type", JsonPrimitive("integer")) - put("description", JsonPrimitive("User age")) - put("default", JsonPrimitive(30)) - }, - ) - put( - "score", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("User score")) - put("default", JsonPrimitive(95.5)) - }, - ) - put( - "status", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("User status")) - put("default", JsonPrimitive("active")) - put( - "enum", - JsonArray( - listOf(JsonPrimitive("active"), JsonPrimitive("inactive"), JsonPrimitive("pending")), - ), - ) - }, - ) - put( - "verified", - buildJsonObject { - put("type", JsonPrimitive("boolean")) - put("description", JsonPrimitive("Verification status")) - put("default", JsonPrimitive(true)) - }, - ) - }, + properties = mapOf( + "name" to StringSchema(description = "User name", default = "John Doe"), + "age" to IntegerSchema(description = "User age", default = 30), + "score" to DoubleSchema(description = "User score", default = 95.5), + "status" to UntitledSingleSelectEnumSchema( + description = "User status", + enumValues = listOf("active", "inactive", "pending"), + default = "active", + ), + "verified" to BooleanSchema(description = "Verification status", default = true), + ), required = listOf("name", "age", "score", "status", "verified"), ) val result = createElicitation( @@ -278,122 +238,43 @@ fun Server.registerConformanceTools() { description = "test_elicitation_sep1330_enums", ) { val schema = ElicitRequestParams.RequestedSchema( - properties = buildJsonObject { + properties = mapOf( // Untitled single-select - put( - "untitledSingle", - buildJsonObject { - put("type", JsonPrimitive("string")) - put( - "enum", - JsonArray( - listOf(JsonPrimitive("option1"), JsonPrimitive("option2"), JsonPrimitive("option3")), - ), - ) - }, - ) + "untitledSingle" to UntitledSingleSelectEnumSchema( + enumValues = listOf("option1", "option2", "option3"), + ), // Titled single-select - put( - "titledSingle", - buildJsonObject { - put("type", JsonPrimitive("string")) - put( - "oneOf", - JsonArray( - listOf( - buildJsonObject { - put("const", JsonPrimitive("value1")) - put("title", JsonPrimitive("First Option")) - }, - buildJsonObject { - put("const", JsonPrimitive("value2")) - put("title", JsonPrimitive("Second Option")) - }, - buildJsonObject { - put("const", JsonPrimitive("value3")) - put("title", JsonPrimitive("Third Option")) - }, - ), - ), - ) - }, - ) - // Legacy titled (deprecated) - uses enum + enumNames arrays - put( - "legacyEnum", - buildJsonObject { - put("type", JsonPrimitive("string")) - put( - "enum", - JsonArray(listOf(JsonPrimitive("opt1"), JsonPrimitive("opt2"), JsonPrimitive("opt3"))), - ) - put( - "enumNames", - JsonArray( - listOf( - JsonPrimitive("Option One"), - JsonPrimitive("Option Two"), - JsonPrimitive("Option Three"), - ), - ), - ) - }, - ) + "titledSingle" to TitledSingleSelectEnumSchema( + oneOf = listOf( + EnumOption(const = "value1", title = "First Option"), + EnumOption(const = "value2", title = "Second Option"), + EnumOption(const = "value3", title = "Third Option"), + ), + ), + // Legacy titled (deprecated) — uses enum + enumNames + @Suppress("DEPRECATION") + "legacyEnum" + to LegacyTitledEnumSchema( + enumValues = listOf("opt1", "opt2", "opt3"), + enumNames = listOf("Option One", "Option Two", "Option Three"), + ), // Untitled multi-select - put( - "untitledMulti", - buildJsonObject { - put("type", JsonPrimitive("array")) - put( - "items", - buildJsonObject { - put("type", JsonPrimitive("string")) - put( - "enum", - JsonArray( - listOf( - JsonPrimitive("option1"), - JsonPrimitive("option2"), - JsonPrimitive("option3"), - ), - ), - ) - }, - ) - }, - ) - // Titled multi-select - uses items.anyOf with const/title pairs - put( - "titledMulti", - buildJsonObject { - put("type", JsonPrimitive("array")) - put( - "items", - buildJsonObject { - put( - "anyOf", - JsonArray( - listOf( - buildJsonObject { - put("const", JsonPrimitive("value1")) - put("title", JsonPrimitive("First Choice")) - }, - buildJsonObject { - put("const", JsonPrimitive("value2")) - put("title", JsonPrimitive("Second Choice")) - }, - buildJsonObject { - put("const", JsonPrimitive("value3")) - put("title", JsonPrimitive("Third Choice")) - }, - ), - ), - ) - }, - ) - }, - ) - }, + "untitledMulti" to UntitledMultiSelectEnumSchema( + items = UntitledMultiSelectEnumSchema.Items( + enumValues = listOf("option1", "option2", "option3"), + ), + ), + // Titled multi-select + "titledMulti" to TitledMultiSelectEnumSchema( + items = TitledMultiSelectEnumSchema.Items( + anyOf = listOf( + EnumOption(const = "value1", title = "First Choice"), + EnumOption(const = "value2", title = "Second Choice"), + EnumOption(const = "value3", title = "Third Choice"), + ), + ), + ), + ), ) val result = createElicitation( "Please review and update the form fields with defaults", @@ -538,54 +419,17 @@ fun Server.registerConformanceTools() { description = "test_client_elicitation_defaults", ) { val schema = ElicitRequestParams.RequestedSchema( - properties = buildJsonObject { - put( - "name", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("User name")) - put("default", JsonPrimitive("John Doe")) - }, - ) - put( - "age", - buildJsonObject { - put("type", JsonPrimitive("integer")) - put("description", JsonPrimitive("User age")) - put("default", JsonPrimitive(30)) - }, - ) - put( - "score", - buildJsonObject { - put("type", JsonPrimitive("number")) - put("description", JsonPrimitive("User score")) - put("default", JsonPrimitive(95.5)) - }, - ) - put( - "status", - buildJsonObject { - put("type", JsonPrimitive("string")) - put("description", JsonPrimitive("User status")) - put("default", JsonPrimitive("active")) - put( - "enum", - JsonArray( - listOf(JsonPrimitive("active"), JsonPrimitive("inactive"), JsonPrimitive("pending")), - ), - ) - }, - ) - put( - "verified", - buildJsonObject { - put("type", JsonPrimitive("boolean")) - put("description", JsonPrimitive("Verification status")) - put("default", JsonPrimitive(true)) - }, - ) - }, + properties = mapOf( + "name" to StringSchema(description = "User name", default = "John Doe"), + "age" to IntegerSchema(description = "User age", default = 30), + "score" to DoubleSchema(description = "User score", default = 95.5), + "status" to UntitledSingleSelectEnumSchema( + description = "User status", + enumValues = listOf("active", "inactive", "pending"), + default = "active", + ), + "verified" to BooleanSchema(description = "Verification status", default = true), + ), required = emptyList(), ) val result = createElicitation( diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index 9d512361e..1d84732a2 100644 --- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -10,6 +10,7 @@ import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject @@ -35,6 +36,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.Root import io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.Tool import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema @@ -48,7 +50,6 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonObject import kotlin.coroutines.cancellation.CancellationException import kotlin.test.Test import kotlin.test.assertEquals @@ -953,11 +954,7 @@ class ClientTest { serverSession.createElicitation( message = "Please provide your GitHub username", requestedSchema = ElicitRequestParams.RequestedSchema( - properties = buildJsonObject { - putJsonObject("name") { - put("type", "string") - } - }, + properties = mapOf("name" to StringSchema()), required = listOf("name"), ), ) @@ -1061,11 +1058,7 @@ class ClientTest { val elicitationMessage = "Please provide your GitHub username" val requestedSchema = ElicitRequestParams.RequestedSchema( - properties = buildJsonObject { - putJsonObject("name") { - put("type", "string") - } - }, + properties = mapOf("name" to StringSchema()), required = listOf("name"), ) @@ -1076,7 +1069,8 @@ class ClientTest { client.setElicitationHandler { request -> assertEquals(elicitationMessage, request.params.message) - assertEquals(requestedSchema, request.params.requestedSchema) + val formParams = request.params as ElicitRequestFormParams + assertEquals(requestedSchema, formParams.requestedSchema) ElicitResult( action = elicitationResultAction, diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index e7cd0b869..0b81586f9 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -336,6 +336,40 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/BlobResourceContents public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/BooleanSchema : io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Boolean; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/Boolean; + public final fun getDescription ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/BooleanSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/BooleanSchema; + 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/types/BooleanSchema;)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/types/BooleanSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;)V @@ -1134,6 +1168,44 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CustomRequest$Compan public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/DoubleSchema : io/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Double; + public final fun component4 ()Ljava/lang/Double; + public final fun component5 ()Ljava/lang/Double; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;)Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/Double; + public final fun getDescription ()Ljava/lang/String; + public final fun getMaximum ()Ljava/lang/Double; + public final fun getMinimum ()Ljava/lang/Double; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/DoubleSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema; + 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/types/DoubleSchema;)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/types/DoubleSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequest : io/modelcontextprotocol/kotlin/sdk/types/ServerRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams;)V @@ -1176,49 +1248,63 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestBuilder public final fun setMessage (Ljava/lang/String;)V } -public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { - public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$Companion; - public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams : io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams$Companion; + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; - public final fun component3-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; - public final fun copy-EDEVuBg (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams; - public static synthetic fun copy-EDEVuBg$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public final fun component4-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-6R815Ik (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; + public static synthetic fun copy-6R815Ik$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; public fun equals (Ljava/lang/Object;)Z - public final fun getMessage ()Ljava/lang/String; + public fun getMessage ()Ljava/lang/String; public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getMode ()Ljava/lang/String; public final fun getRequestedSchema ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$$serializer; +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams$$serializer; public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; 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/types/ElicitRequestParams;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams;)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/types/ElicitRequestFormParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$Companion; + public abstract fun getMessage ()Ljava/lang/String; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema$Companion; - public fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun component2 ()Ljava/util/List; - public final fun copy (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public fun (Ljava/lang/String;Ljava/util/Map;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Map;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Map; + public final fun component3 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; public fun equals (Ljava/lang/Object;)Z - public final fun getProperties ()Lkotlinx/serialization/json/JsonObject; + public final fun getProperties ()Ljava/util/Map; public final fun getRequired ()Ljava/util/List; + public final fun getSchema ()Ljava/lang/String; public final fun getType ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -1239,6 +1325,43 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$ public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams : io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams$Companion; + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public final fun component5-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun copy-4asOmiE (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams; + public static synthetic fun copy-4asOmiE$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getElicitationId ()Ljava/lang/String; + public fun getMessage ()Ljava/lang/String; + public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; + public final fun getMode ()Ljava/lang/String; + public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata; + public final fun getUrl ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestURLParams; + 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/types/ElicitRequestURLParams;)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/types/ElicitRequestURLParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestedSchemaBuilder { public fun ()V public final fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema; @@ -1294,6 +1417,11 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitationKt { + public static final fun ElicitRequestParams-EDEVuBg (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; + public static synthetic fun ElicitRequestParams-EDEVuBg$default (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/Elicitation_dslKt { public static final fun buildElicitRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest; } @@ -1360,6 +1488,43 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/EmptyResult$Companio public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/EnumOption { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption; + public fun equals (Ljava/lang/Object;)Z + public final fun getConst ()Ljava/lang/String; + public final fun getTitle ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/EnumOption$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/EnumOption; + 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/types/EnumOption;)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/types/EnumOption$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition : io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestParams;)V @@ -1955,6 +2120,44 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/InitializedNotificat public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/IntegerSchema : io/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;)Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/Integer; + public final fun getDescription ()Ljava/lang/String; + public final fun getMaximum ()Ljava/lang/Integer; + public final fun getMinimum ()Ljava/lang/Integer; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/IntegerSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/IntegerSchema; + 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/types/IntegerSchema;)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/types/IntegerSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/JSONRPCEmptyMessage : io/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage { public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCEmptyMessage; public fun equals (Ljava/lang/Object;)Z @@ -2119,6 +2322,43 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/JsonUtilsKt { public static final fun toJson (Ljava/util/Map;)Ljava/util/Map; } +public final class io/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ljava/util/List; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/String; + public final fun getDescription ()Ljava/lang/String; + public final fun getEnumNames ()Ljava/util/List; + public final fun getEnumValues ()Ljava/util/List; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/LegacyTitledEnumSchema; + 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/types/LegacyTitledEnumSchema;)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/types/LegacyTitledEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest, io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest$Companion; public fun ()V @@ -2796,6 +3036,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ModelPreferences$Com public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/Notification { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Notification$Companion; public abstract fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; @@ -2814,6 +3062,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/NotificationParams$C public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition : io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest : io/modelcontextprotocol/kotlin/sdk/types/Request { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PaginatedRequest$Companion; public fun getCursor ()Ljava/lang/String; @@ -2917,6 +3173,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequest_dslKt { public static final fun buildPingRequest (Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest; } +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/Progress { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Progress$Companion; public fun (DLjava/lang/Double;Ljava/lang/String;)V @@ -4141,6 +4405,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestParam public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/SingleSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/EnumSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SingleSelectEnumSchema$Companion; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SingleSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/StopReason { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/StopReason$Companion; public static final synthetic fun box-impl (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/StopReason; @@ -4174,6 +4446,61 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/StopReason$Companion public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/StringSchema : io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaDefinition { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema$Companion; + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public final fun component6 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/String; + public final fun getDescription ()Ljava/lang/String; + public final fun getFormat ()Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public final fun getMaxLength ()Ljava/lang/Integer; + public final fun getMinLength ()Ljava/lang/Integer; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/StringSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/StringSchema; + 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/types/StringSchema;)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/types/StringSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat : java/lang/Enum { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat$Companion; + public static final field Date Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public static final field DateTime Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public static final field Email Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public static final field Uri Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/StringSchemaFormat$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest$Companion; public fun (Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestParams;)V @@ -4503,6 +4830,107 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/TextResourceContents public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items; + public final fun component6 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/util/List; + public final fun getDescription ()Ljava/lang/String; + public final fun getItems ()Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items; + public final fun getMaxItems ()Ljava/lang/Integer; + public final fun getMinItems ()Ljava/lang/Integer; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema; + 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/types/TitledMultiSelectEnumSchema;)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/types/TitledMultiSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items$Companion; + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnyOf ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledMultiSelectEnumSchema$Items; + 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/types/TitledMultiSelectEnumSchema$Items;)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/types/TitledMultiSelectEnumSchema$Items$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/SingleSelectEnumSchema { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/String; + public final fun getDescription ()Ljava/lang/String; + public final fun getOneOf ()Ljava/util/List; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/TitledSingleSelectEnumSchema; + 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/types/TitledSingleSelectEnumSchema;)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/types/TitledSingleSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/Tool : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/Tool$Companion; public fun (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution;Lkotlinx/serialization/json/JsonObject;)V @@ -4793,6 +5221,108 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestPa public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/Integer; + public final fun component4 ()Ljava/lang/Integer; + public final fun component5 ()Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items; + public final fun component6 ()Ljava/util/List; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/util/List; + public final fun getDescription ()Ljava/lang/String; + public final fun getItems ()Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items; + public final fun getMaxItems ()Ljava/lang/Integer; + public final fun getMinItems ()Ljava/lang/Integer; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema; + 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/types/UntitledMultiSelectEnumSchema;)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/types/UntitledMultiSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items$Companion; + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items; + public fun equals (Ljava/lang/Object;)Z + public final fun getEnumValues ()Ljava/util/List; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items; + 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/types/UntitledMultiSelectEnumSchema$Items;)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/types/UntitledMultiSelectEnumSchema$Items$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/SingleSelectEnumSchema { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema; + public fun equals (Ljava/lang/Object;)Z + public final fun getDefault ()Ljava/lang/String; + public final fun getDescription ()Ljava/lang/String; + public final fun getEnumValues ()Ljava/util/List; + public final fun getTitle ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelectEnumSchema; + 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/types/UntitledSingleSelectEnumSchema;)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/types/UntitledSingleSelectEnumSchema$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/WithMeta { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/WithMeta$Companion; public abstract fun getMeta ()Lkotlinx/serialization/json/JsonObject; diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt index e398d98e9..12309fad7 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt @@ -4,6 +4,7 @@ import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObjectBuilder import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.decodeFromJsonElement import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -223,6 +224,9 @@ public class ElicitRequestedSchemaBuilder @PublishedApi internal constructor() { val properties = requireNotNull(properties) { "Missing required field 'properties'. Use properties { put(\"fieldName\", schema) }" } - return ElicitRequestParams.RequestedSchema(properties, required) + val typedProperties: Map = properties.mapValues { (_, value) -> + McpJson.decodeFromJsonElement(value) + } + return ElicitRequestParams.RequestedSchema(properties = typedProperties, required = required) } } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt index dfda9bf16..fc7787b49 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt @@ -1,23 +1,20 @@ package io.modelcontextprotocol.kotlin.sdk.types import kotlinx.serialization.EncodeDefault -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject /** - * A request from the server to elicit additional information from the user via the client. + * Represents an `elicitation/create` request from the server to the client. * - * This request type allows servers to prompt users for structured input through forms - * or dialogs presented by the client. The server defines a schema for the requested data, - * and the client presents an appropriate UI to collect this information. + * Supports two modes: form mode ([ElicitRequestFormParams]) for collecting structured data + * in-band, and URL mode ([ElicitRequestURLParams]) for directing the user to an external URL. * - * @property params The elicitation parameters including the message and requested schema. + * @property params The elicitation parameters — either form or URL mode. */ @Serializable public data class ElicitRequest(override val params: ElicitRequestParams) : ServerRequest { - @OptIn(ExperimentalSerializationApi::class) @EncodeDefault public override val method: Method = Method.Defined.ElicitationCreate @@ -30,8 +27,13 @@ public data class ElicitRequest(override val params: ElicitRequestParams) : Serv /** * A restricted subset of JSON Schema defining the structure of the requested data. */ - public val requestedSchema: ElicitRequestParams.RequestedSchema - get() = params.requestedSchema + @Deprecated( + "Use (params as ElicitRequestFormParams).requestedSchema", + ReplaceWith("(params as ElicitRequestFormParams).requestedSchema"), + DeprecationLevel.WARNING, + ) + public val requestedSchema: ElicitRequestParams.RequestedSchema? + get() = (params as? ElicitRequestFormParams)?.requestedSchema /** * Metadata for this request. May include a progressToken for out-of-band progress notifications. @@ -41,23 +43,13 @@ public data class ElicitRequest(override val params: ElicitRequestParams) : Serv } /** - * Parameters for an elicitation/create request. + * Represents the parameters for an `elicitation/create` request. * - * @property message The message to present to the user. This should clearly explain - * what information is being requested and why. - * @property requestedSchema A restricted subset of JSON Schema defining the structure - * of the requested data. Only top-level properties are allowed, - * without nesting. - * @property meta Optional metadata for this request. May include a progressToken for - * out-of-band progress notifications. + * Implementations: [ElicitRequestFormParams], [ElicitRequestURLParams]. */ -@Serializable -public data class ElicitRequestParams( - val message: String, - val requestedSchema: RequestedSchema, - @SerialName("_meta") - override val meta: RequestMeta? = null, -) : RequestParams { +@Serializable(with = ElicitRequestParamsSerializer::class) +public sealed interface ElicitRequestParams : RequestParams { + public val message: String /** * A restricted JSON Schema for elicitation requests. @@ -65,6 +57,7 @@ public data class ElicitRequestParams( * Only supports top-level primitive properties without nesting. Each property * represents a field in the form or dialog presented to the user. * + * @property schema Optional URI to a JSON Schema definition. * @property properties A map of property names to their schema definitions. * Each property must be a primitive type (string, number, boolean). * @property required Optional list of property names that must be provided by the user. @@ -72,24 +65,102 @@ public data class ElicitRequestParams( * @property type Always "object" for elicitation schemas. */ @Serializable - public data class RequestedSchema(val properties: JsonObject, val required: List? = null) { - @OptIn(ExperimentalSerializationApi::class) + public data class RequestedSchema( + @SerialName($$"$schema") + val schema: String? = null, + val properties: Map, + val required: List? = null, + ) { @EncodeDefault val type: String = "object" } } /** - * The client's response to an [ElicitRequest]. + * Creates an [ElicitRequestFormParams] for backwards compatibility. + * + * @param message The message to present to the user. + * @param requestedSchema The JSON Schema for the requested data. + * @param meta Optional request metadata. + * @return A configured [ElicitRequestFormParams] instance. + */ +@Deprecated( + "Use ElicitRequestFormParams instead", + ReplaceWith("ElicitRequestFormParams(message, requestedSchema = requestedSchema, meta = meta)"), + DeprecationLevel.WARNING, +) +@Suppress("FunctionNaming", "FunctionName") +public fun ElicitRequestParams( + message: String, + requestedSchema: ElicitRequestParams.RequestedSchema, + meta: RequestMeta? = null, +): ElicitRequestFormParams = ElicitRequestFormParams( + message = message, + requestedSchema = requestedSchema, + meta = meta, +) + +/** + * Represents form mode parameters for an `elicitation/create` request. + * + * Collects non-sensitive structured data from the user via a form presented by the client. * - * Represents the user's action and, if accepted, the submitted form data. + * @property message The message to present to the user describing what information is being requested. + * @property task If specified, the caller is requesting task-augmented execution. The request + * will return a [CreateTaskResult] immediately, and the actual result can be retrieved + * later via `tasks/result`. + * @property requestedSchema A restricted subset of JSON Schema. Only top-level properties + * are allowed, without nesting. + * @property meta Optional metadata. May include a progressToken for out-of-band progress notifications. + */ +@Serializable +public data class ElicitRequestFormParams( + override val message: String, + val task: TaskMetadata? = null, + val requestedSchema: ElicitRequestParams.RequestedSchema, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : ElicitRequestParams { + @EncodeDefault + public val mode: String = "form" +} + +/** + * Represents URL mode parameters for an `elicitation/create` request. + * + * Directs the user to an external URL for out-of-band interactions (e.g., OAuth flows, + * payment processing, or entering sensitive credentials) that must not pass through the MCP client. + * + * @property message The message explaining why the interaction is needed. + * @property elicitationId A unique identifier for this elicitation. The client MUST treat + * this ID as an opaque value. + * @property url The URL that the user should navigate to. + * @property task If specified, the caller is requesting task-augmented execution. The request + * will return a [CreateTaskResult] immediately, and the actual result can be retrieved + * later via `tasks/result`. + * @property meta Optional metadata. May include a progressToken for out-of-band progress notifications. + */ +@Serializable +public data class ElicitRequestURLParams( + override val message: String, + val elicitationId: String, + val url: String, + val task: TaskMetadata? = null, + @SerialName("_meta") + override val meta: RequestMeta? = null, +) : ElicitRequestParams { + @EncodeDefault + public val mode: String = "url" +} + +/** + * Represents the client's response to an [ElicitRequest]. * - * @property action The user action in response to the elicitation prompt. - * @property content The submitted form data, only present when [action] is [Action.Accept]. - * Contains values matching the requested schema, where keys correspond - * to property names and values are primitives (string, number, or boolean). + * @property action The user action in response to the elicitation. + * @property content The submitted form data, only present when [action] is [Action.Accept] + * and mode was form. Contains values matching the requested schema. Omitted for + * URL mode responses. * @property meta Optional metadata for this response. - * @throws IllegalArgumentException if content is provided with a non-accept action. */ @Serializable public data class ElicitResult( diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt new file mode 100644 index 000000000..dda2ac6d4 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt @@ -0,0 +1,292 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Restricted schema definition for elicitation request properties. + * + * Supports primitive types (string, number, integer, boolean) and enum schemas + * (single-select and multi-select). Only flat, top-level properties are allowed — + * arbitrary nested objects or arrays are not supported. + * + * Implementations: [StringSchema], [NumberSchemaDefinition], [BooleanSchema], [EnumSchemaDefinition]. + */ +@Serializable(with = PrimitiveSchemaDefinitionSerializer::class) +public sealed interface PrimitiveSchemaDefinition + +/** + * Defines a string-typed property in an elicitation schema. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property minLength Minimum string length. + * @property maxLength Maximum string length. + * @property format Optional format constraint (e.g., email, URI, date). + * @property default Optional default value. + */ +@Serializable +public data class StringSchema( + val title: String? = null, + val description: String? = null, + val minLength: Int? = null, + val maxLength: Int? = null, + val format: StringSchemaFormat? = null, + val default: String? = null, +) : PrimitiveSchemaDefinition { + @EncodeDefault + public val type: String = "string" +} + +/** + * Supported format constraints for [StringSchema]. + */ +@Serializable +public enum class StringSchemaFormat { + @SerialName("email") + Email, + + @SerialName("uri") + Uri, + + @SerialName("date") + Date, + + @SerialName("date-time") + DateTime, +} + +/** + * Defines a numeric property in an elicitation schema. + * + * Implementations: [IntegerSchema] (`"integer"`), [DoubleSchema] (`"number"`). + */ +@Serializable +public sealed interface NumberSchemaDefinition : PrimitiveSchemaDefinition + +/** + * Defines an integer-typed property in an elicitation schema. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property minimum Minimum allowed value. + * @property maximum Maximum allowed value. + * @property default Optional default value. + */ +@Serializable +public data class IntegerSchema( + val title: String? = null, + val description: String? = null, + val minimum: Int? = null, + val maximum: Int? = null, + val default: Int? = null, +) : NumberSchemaDefinition { + @EncodeDefault + val type: String = "integer" +} + +/** + * Defines a floating-point number property in an elicitation schema. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property minimum Minimum allowed value. + * @property maximum Maximum allowed value. + * @property default Optional default value. + */ +@Serializable +public data class DoubleSchema( + val title: String? = null, + val description: String? = null, + val minimum: Double? = null, + val maximum: Double? = null, + val default: Double? = null, +) : NumberSchemaDefinition { + @EncodeDefault + val type: String = "number" +} + +/** + * Defines a boolean-typed property in an elicitation schema. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property default Optional default value. + */ +@Serializable +public data class BooleanSchema( + val title: String? = null, + val description: String? = null, + val default: Boolean? = null, +) : PrimitiveSchemaDefinition { + @EncodeDefault + val type: String = "boolean" +} + +/** + * Defines an enumeration property in an elicitation schema. + * + * Implementations: [SingleSelectEnumSchema], [MultiSelectEnumSchema], [LegacyTitledEnumSchema]. + */ +@Serializable +public sealed interface EnumSchemaDefinition : PrimitiveSchemaDefinition + +/** + * Represents an enum option with a value and a display label. + * + * Used in [TitledSingleSelectEnumSchema] and [TitledMultiSelectEnumSchema]. + * + * @property const The enum value. + * @property title Display label for this option. + */ +@Serializable +public data class EnumOption(val const: String, val title: String) + +/** + * Defines a single-selection enumeration property. + * + * Implementations: [UntitledSingleSelectEnumSchema], [TitledSingleSelectEnumSchema]. + */ +@Serializable +public sealed interface SingleSelectEnumSchema : EnumSchemaDefinition + +/** + * Defines a single-selection enumeration without display titles for options. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property enumValues Array of enum values to choose from. + * @property default Optional default value. + */ +@Serializable +public data class UntitledSingleSelectEnumSchema( + val title: String? = null, + val description: String? = null, + @SerialName("enum") + val enumValues: List, + val default: String? = null, +) : SingleSelectEnumSchema { + @EncodeDefault + val type: String = "string" +} + +/** + * Defines a single-selection enumeration with display titles for each option. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property oneOf Array of enum options with values and display labels. + * @property default Optional default value. + */ +@Serializable +public data class TitledSingleSelectEnumSchema( + val title: String? = null, + val description: String? = null, + val oneOf: List, + val default: String? = null, +) : SingleSelectEnumSchema { + @EncodeDefault + val type: String = "string" +} + +/** + * Defines a single-selection enumeration with display names via the deprecated `enumNames` array. + * + * Use [TitledSingleSelectEnumSchema] instead. This class will be removed in a future version. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property enumValues Array of enum values to choose from. + * @property enumNames Display names for enum values. Non-standard according to JSON Schema 2020-12. + * @property default Optional default value. + */ +@Deprecated("Use TitledSingleSelectEnumSchema instead") +@Serializable +public data class LegacyTitledEnumSchema( + val title: String? = null, + val description: String? = null, + @SerialName("enum") + val enumValues: List, + val enumNames: List? = null, + val default: String? = null, +) : EnumSchemaDefinition { + @EncodeDefault + val type: String = "string" +} + +/** + * Defines a multiple-selection enumeration property. + * + * Implementations: [UntitledMultiSelectEnumSchema], [TitledMultiSelectEnumSchema]. + */ +@Serializable +public sealed interface MultiSelectEnumSchema : EnumSchemaDefinition + +/** + * Defines a multiple-selection enumeration without display titles for options. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property minItems Minimum number of items to select. + * @property maxItems Maximum number of items to select. + * @property items Schema for the array items. + * @property default Optional default value. + */ +@Serializable +public data class UntitledMultiSelectEnumSchema( + val title: String? = null, + val description: String? = null, + val minItems: Int? = null, + val maxItems: Int? = null, + val items: Items, + val default: List? = null, +) : MultiSelectEnumSchema { + @EncodeDefault + val type: String = "array" + + /** + * Schema for the array items with plain enum values. + * + * @property enumValues Array of enum values to choose from. + */ + @Serializable + public data class Items( + @SerialName("enum") + val enumValues: List, + ) { + @EncodeDefault + val type: String = "string" + } +} + +/** + * Defines a multiple-selection enumeration with display titles for each option. + * + * @property title Optional display title for the field. + * @property description Optional description for the field. + * @property minItems Minimum number of items to select. + * @property maxItems Maximum number of items to select. + * @property items Schema for array items with enum options and display labels. + * @property default Optional default value. + */ +@Serializable +public data class TitledMultiSelectEnumSchema( + val title: String? = null, + val description: String? = null, + val minItems: Int? = null, + val maxItems: Int? = null, + val items: Items, + val default: List? = null, +) : MultiSelectEnumSchema { + @EncodeDefault + val type: String = "array" + + /** + * Schema for array items with enum options and display labels. + * + * @property anyOf Array of enum options with values and display labels. + */ + @Serializable + public data class Items(val anyOf: List) +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt index 792c63605..7f1c81dff 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt @@ -12,6 +12,7 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonContentPolymorphicSerializer import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.longOrNull @@ -420,3 +421,80 @@ internal object RequestIdPolymorphicSerializer : JsonContentPolymorphicSerialize else -> throw SerializationException("Invalid RequestId type: $element") } } + +// ============================================================================ +// ElicitationParams Serializers +// ============================================================================ + +internal object ElicitRequestParamsSerializer : JsonContentPolymorphicSerializer( + ElicitRequestParams::class, +) { + @Suppress("ThrowsCount") + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + val mode = when (val modeElement = element.jsonObject["mode"]) { + null -> null + + is JsonPrimitive -> { + if (!modeElement.isString) { + throw SerializationException("Invalid 'mode' type: expected string but was: $modeElement") + } + modeElement.contentOrNull + } + + else -> throw SerializationException("Invalid 'mode' type: expected string but was: $modeElement") + } + return when (mode) { + "url" -> ElicitRequestURLParams.serializer() + "form", null -> ElicitRequestFormParams.serializer() + else -> throw SerializationException("Unsupported elicitation mode: '$mode'") + } + } +} + +// ============================================================================ +// PrimitiveSchemaDefinition Serializers +// ============================================================================ + +internal object PrimitiveSchemaDefinitionSerializer : JsonContentPolymorphicSerializer( + PrimitiveSchemaDefinition::class, +) { + @Suppress("ThrowsCount") + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + val obj = element.jsonObject + val typeElement = obj["type"] + ?: throw SerializationException("Missing 'type' in PrimitiveSchemaDefinition: $element") + val type = if (typeElement is JsonPrimitive && typeElement.isString) { + typeElement.content + } else { + throw SerializationException("Expected 'type' to be a string but was: $typeElement") + } + return when (type) { + "boolean" -> BooleanSchema.serializer() + "integer" -> IntegerSchema.serializer() + "number" -> DoubleSchema.serializer() + "string" -> selectStringTypeDeserializer(obj) + "array" -> selectArrayTypeDeserializer(obj) + else -> throw SerializationException("Unknown PrimitiveSchemaDefinition type: '$type'") + } + } + + private fun selectArrayTypeDeserializer( + obj: Map, + ): DeserializationStrategy { + val items = obj["items"]?.jsonObject + return when { + items != null && "anyOf" in items -> TitledMultiSelectEnumSchema.serializer() + else -> UntitledMultiSelectEnumSchema.serializer() + } + } + + @Suppress("DEPRECATION") + private fun selectStringTypeDeserializer( + obj: Map, + ): DeserializationStrategy = when { + "enumNames" in obj -> LegacyTitledEnumSchema.serializer() + "enum" in obj -> UntitledSingleSelectEnumSchema.serializer() + "oneOf" in obj -> TitledSingleSelectEnumSchema.serializer() + else -> StringSchema.serializer() + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt index 0026ea0be..5392890df 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt @@ -1,58 +1,39 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.kotest.assertions.json.shouldEqualJson +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotNull -import kotlin.test.assertNull class ElicitationTest { + // ── Form mode ─────────────────────────────────────────────────────── + @Test - @Suppress("LongMethod") - fun `should serialize ElicitRequest with requested schema`() { + fun `should serialize form mode ElicitRequest`() { val request = ElicitRequest( - ElicitRequestParams( + ElicitRequestFormParams( message = "Provide repository details", requestedSchema = ElicitRequestParams.RequestedSchema( - properties = buildJsonObject { - put( - "owner", - buildJsonObject { - put("type", "string") - put("description", "GitHub organization") - }, - ) - put( - "repository", - buildJsonObject { - put("type", "string") - put("title", "Repository name") - }, - ) - put( - "private", - buildJsonObject { - put("type", "boolean") - put("description", "Is the repository private?") - }, - ) - }, + properties = mapOf( + "owner" to StringSchema(description = "GitHub organization"), + "repository" to StringSchema(title = "Repository name"), + "private" to BooleanSchema(description = "Is the repository private?"), + ), required = listOf("owner", "repository"), ), meta = RequestMeta( - buildJsonObject { - put("progressToken", "token-42") - }, + buildJsonObject { put("progressToken", "token-42") }, ), ), ) @@ -68,16 +49,16 @@ class ElicitationTest { "requestedSchema": { "properties": { "owner": { - "type": "string", - "description": "GitHub organization" + "description": "GitHub organization", + "type": "string" }, "repository": { - "type": "string", - "title": "Repository name" + "title": "Repository name", + "type": "string" }, "private": { - "type": "boolean", - "description": "Is the repository private?" + "description": "Is the repository private?", + "type": "boolean" } }, "required": ["owner", "repository"], @@ -85,7 +66,8 @@ class ElicitationTest { }, "_meta": { "progressToken": "token-42" - } + }, + "mode": "form" } } """.trimIndent(), @@ -93,22 +75,16 @@ class ElicitationTest { } @Test - fun `should deserialize ElicitRequest from JSON`() { + fun `should deserialize form mode ElicitRequest with explicit mode`() { val json = """ { "method": "elicitation/create", "params": { - "message": "Tell us about the repository", + "mode": "form", + "message": "Enter your name", "requestedSchema": { "properties": { - "name": { - "type": "string", - "title": "Repository name" - }, - "stars": { - "type": "number", - "description": "GitHub stars" - } + "name": {"type": "string", "title": "Full name"} }, "type": "object" } @@ -117,27 +93,121 @@ class ElicitationTest { """.trimIndent() val request = verifyDeserialization(McpJson, json) - assertEquals(Method.Defined.ElicitationCreate, request.method) + request.method shouldBe Method.Defined.ElicitationCreate + request.message shouldBe "Enter your name" + + val params = request.params.shouldBeInstanceOf() + val nameSchema = params.requestedSchema.properties["name"].shouldBeInstanceOf() + nameSchema.title shouldBe "Full name" + } + + @Test + fun `should deserialize form mode ElicitRequest without mode field`() { + val json = """ + { + "method": "elicitation/create", + "params": { + "message": "Enter your name", + "requestedSchema": { + "properties": { + "name": {"type": "string"} + }, + "type": "object" + } + } + } + """.trimIndent() + + val request = McpJson.decodeFromString(json) + request.params.shouldBeInstanceOf() + request.params.message shouldBe "Enter your name" + } + + // ── URL mode ──────────────────────────────────────────────────────── + + @Test + fun `should serialize URL mode ElicitRequest`() { + val request = ElicitRequest( + ElicitRequestURLParams( + message = "Please provide your API key", + elicitationId = "550e8400-e29b-41d4-a716-446655440000", + url = "https://mcp.example.com/ui/set_api_key", + ), + ) + + verifySerialization( + request, + McpJson, + """ + { + "method": "elicitation/create", + "params": { + "message": "Please provide your API key", + "elicitationId": "550e8400-e29b-41d4-a716-446655440000", + "url": "https://mcp.example.com/ui/set_api_key", + "mode": "url" + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize URL mode ElicitRequest`() { + val json = """ + { + "method": "elicitation/create", + "params": { + "mode": "url", + "message": "Authorize access", + "elicitationId": "id-123", + "url": "https://example.com/auth" + } + } + """.trimIndent() + + val request = verifyDeserialization(McpJson, json) + val params = request.params.shouldBeInstanceOf() + params.message shouldBe "Authorize access" + params.elicitationId shouldBe "id-123" + params.url shouldBe "https://example.com/auth" + } - val params = request.params - assertEquals("Tell us about the repository", params.message) - assertNull(params.meta) + // ── Deprecated compat ─────────────────────────────────────────────── - val schema = params.requestedSchema - assertEquals("object", schema.type) - assertNull(schema.required) + @Test + @Suppress("DEPRECATION") + fun `deprecated factory function should create ElicitRequestFormParams`() { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + ) + val params = ElicitRequestParams(message = "Test", requestedSchema = schema) + params.shouldBeInstanceOf() + params.message shouldBe "Test" + params.requestedSchema shouldBe schema + } - val nameDefinition = schema.properties["name"]?.jsonObject - assertNotNull(nameDefinition) - assertEquals("string", nameDefinition["type"]?.jsonPrimitive?.content) - assertEquals("Repository name", nameDefinition["title"]?.jsonPrimitive?.content) + @Test + @Suppress("DEPRECATION") + fun `deprecated requestedSchema should return schema for form mode`() { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf("x" to BooleanSchema()), + ) + val request = ElicitRequest(ElicitRequestFormParams(message = "m", requestedSchema = schema)) + request.requestedSchema shouldBe schema + } - val starsDefinition = schema.properties["stars"]?.jsonObject - assertNotNull(starsDefinition) - assertEquals("number", starsDefinition["type"]?.jsonPrimitive?.content) - assertEquals("GitHub stars", starsDefinition["description"]?.jsonPrimitive?.content) + @Test + @Suppress("DEPRECATION") + fun `deprecated requestedSchema should return null for URL mode`() { + val request = ElicitRequest( + ElicitRequestURLParams(message = "m", elicitationId = "id", url = "https://example.com"), + ) + request.requestedSchema.shouldBeNull() } + // ── ElicitResult ──────────────────────────────────────────────────── + @Test fun `should serialize and deserialize accept result with content`() { val result = ElicitResult( @@ -169,17 +239,15 @@ class ElicitationTest { """.trimIndent() val decoded = verifyDeserialization(McpJson, json) - assertEquals(ElicitResult.Action.Accept, decoded.action) + decoded.action shouldBe ElicitResult.Action.Accept - val content = decoded.content - assertNotNull(content) - assertEquals("kotlin-sdk", content["repository"]?.jsonPrimitive?.content) - assertEquals(128, content["stars"]?.jsonPrimitive?.int) - assertEquals(false, content["private"]?.jsonPrimitive?.boolean) + val content = decoded.content.shouldNotBeNull() + content["repository"]?.jsonPrimitive?.content shouldBe "kotlin-sdk" + content["stars"]?.jsonPrimitive?.int shouldBe 128 + content["private"]?.jsonPrimitive?.boolean shouldBe false - val meta = decoded.meta - assertNotNull(meta) - assertEquals("2025-01-12T15:00:58Z", meta["submittedAt"]?.jsonPrimitive?.content) + val meta = decoded.meta.shouldNotBeNull() + meta["submittedAt"]?.jsonPrimitive?.content shouldBe "2025-01-12T15:00:58Z" } @Test @@ -194,14 +262,14 @@ class ElicitationTest { """.trimIndent() val result = verifyDeserialization(McpJson, json) - assertEquals(ElicitResult.Action.Decline, result.action) - assertNull(result.content) - assertEquals("User skipped", result.meta?.get("reason")?.jsonPrimitive?.content) + result.action shouldBe ElicitResult.Action.Decline + result.content.shouldBeNull() + result.meta?.get("reason")?.jsonPrimitive?.content shouldBe "User skipped" } @Test fun `should require content only for accept action`() { - assertFailsWith { + shouldThrow { ElicitResult( action = ElicitResult.Action.Cancel, content = buildJsonObject { put("value", "ignored") }, diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt new file mode 100644 index 000000000..954fb1ef5 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt @@ -0,0 +1,481 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.types.shouldBeInstanceOf +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.SerializationException +import kotlin.test.Test + +class PrimitiveSchemaTest { + + // ── StringSchema ──────────────────────────────────────────────────── + + @Test + fun `should serialize StringSchema with minimal fields`() { + verifySerialization( + StringSchema() as PrimitiveSchemaDefinition, + McpJson, + """{"type": "string"}""", + ) + } + + @Test + fun `should serialize StringSchema with all fields`() { + val schema = StringSchema( + title = "Email address", + description = "User email", + minLength = 5, + maxLength = 100, + format = StringSchemaFormat.Email, + default = "user@example.com", + ) + + verifySerialization( + schema as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Email address", + "description": "User email", + "minLength": 5, + "maxLength": 100, + "format": "email", + "default": "user@example.com", + "type": "string" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize StringSchema from JSON`() { + val json = """ + { + "type": "string", + "title": "Name", + "format": "uri" + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.title shouldBe "Name" + schema.format shouldBe StringSchemaFormat.Uri + } + + // ── StringSchemaFormat ────────────────────────────────────────────── + + @Test + fun `should serialize all StringSchemaFormat values`() { + val cases = mapOf( + StringSchemaFormat.Email to "email", + StringSchemaFormat.Uri to "uri", + StringSchemaFormat.Date to "date", + StringSchemaFormat.DateTime to "date-time", + ) + for ((format, expectedValue) in cases) { + val schema = StringSchema(format = format) + val json = McpJson.encodeToString(schema) + json shouldContain "\"format\":\"$expectedValue\"" + } + } + + // ── IntegerSchema ─────────────────────────────────────────────────── + + @Test + fun `should serialize IntegerSchema with minimal fields`() { + verifySerialization( + IntegerSchema() as PrimitiveSchemaDefinition, + McpJson, + """{"type": "integer"}""", + ) + } + + @Test + fun `should serialize IntegerSchema with all fields`() { + verifySerialization( + IntegerSchema( + title = "Age", + description = "User age", + minimum = 0, + maximum = 150, + default = 25, + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Age", + "description": "User age", + "minimum": 0, + "maximum": 150, + "default": 25, + "type": "integer" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize IntegerSchema from JSON`() { + val json = """{"type": "integer", "minimum": 1, "maximum": 10}""" + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.minimum shouldBe 1 + schema.maximum shouldBe 10 + } + + // ── DoubleSchema ──────────────────────────────────────────────────── + + @Test + fun `should serialize DoubleSchema with minimal fields`() { + verifySerialization( + DoubleSchema() as PrimitiveSchemaDefinition, + McpJson, + """{"type": "number"}""", + ) + } + + @Test + fun `should serialize DoubleSchema with all fields`() { + verifySerialization( + DoubleSchema( + title = "Score", + description = "Test score", + minimum = 0.0, + maximum = 100.0, + default = 50.0, + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Score", + "description": "Test score", + "minimum": 0.0, + "maximum": 100.0, + "default": 50.0, + "type": "number" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize DoubleSchema from JSON`() { + val json = """{"type": "number", "minimum": 0.5}""" + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.minimum shouldBe 0.5 + } + + // ── BooleanSchema ─────────────────────────────────────────────────── + + @Test + fun `should serialize BooleanSchema with minimal fields`() { + verifySerialization( + BooleanSchema() as PrimitiveSchemaDefinition, + McpJson, + """{"type": "boolean"}""", + ) + } + + @Test + fun `should serialize BooleanSchema with all fields`() { + verifySerialization( + BooleanSchema( + title = "Agree", + description = "Terms acceptance", + default = false, + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Agree", + "description": "Terms acceptance", + "default": false, + "type": "boolean" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize BooleanSchema from JSON`() { + val json = """{"type": "boolean", "default": true}""" + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.default shouldBe true + } + + // ── UntitledSingleSelectEnumSchema ────────────────────────────────── + + @Test + fun `should serialize UntitledSingleSelectEnumSchema`() { + verifySerialization( + UntitledSingleSelectEnumSchema( + title = "Color", + enumValues = listOf("Red", "Green", "Blue"), + default = "Red", + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Color", + "enum": ["Red", "Green", "Blue"], + "default": "Red", + "type": "string" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize UntitledSingleSelectEnumSchema from JSON`() { + val json = """ + { + "type": "string", + "enum": ["A", "B", "C"] + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.enumValues shouldBe listOf("A", "B", "C") + } + + // ── TitledSingleSelectEnumSchema ──────────────────────────────────── + + @Test + fun `should serialize TitledSingleSelectEnumSchema`() { + verifySerialization( + TitledSingleSelectEnumSchema( + title = "Color", + oneOf = listOf( + EnumOption(const = "#FF0000", title = "Red"), + EnumOption(const = "#00FF00", title = "Green"), + ), + default = "#FF0000", + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Color", + "oneOf": [ + {"const": "#FF0000", "title": "Red"}, + {"const": "#00FF00", "title": "Green"} + ], + "default": "#FF0000", + "type": "string" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize TitledSingleSelectEnumSchema from JSON`() { + val json = """ + { + "type": "string", + "oneOf": [ + {"const": "a", "title": "Alpha"}, + {"const": "b", "title": "Beta"} + ] + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.oneOf shouldHaveSize 2 + schema.oneOf[0].const shouldBe "a" + schema.oneOf[0].title shouldBe "Alpha" + } + + // ── UntitledMultiSelectEnumSchema ──────────────────────────────────── + + @Test + fun `should serialize UntitledMultiSelectEnumSchema`() { + verifySerialization( + UntitledMultiSelectEnumSchema( + title = "Colors", + minItems = 1, + maxItems = 3, + items = UntitledMultiSelectEnumSchema.Items(enumValues = listOf("Red", "Green", "Blue")), + default = listOf("Red"), + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Colors", + "minItems": 1, + "maxItems": 3, + "items": { + "enum": ["Red", "Green", "Blue"], + "type": "string" + }, + "default": ["Red"], + "type": "array" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize UntitledMultiSelectEnumSchema from JSON`() { + val json = """ + { + "type": "array", + "items": { + "type": "string", + "enum": ["X", "Y"] + } + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.items.enumValues shouldBe listOf("X", "Y") + } + + // ── TitledMultiSelectEnumSchema ────────────────────────────────────── + + @Test + fun `should serialize TitledMultiSelectEnumSchema`() { + verifySerialization( + TitledMultiSelectEnumSchema( + title = "Colors", + minItems = 1, + maxItems = 2, + items = TitledMultiSelectEnumSchema.Items( + anyOf = listOf( + EnumOption(const = "#FF0000", title = "Red"), + EnumOption(const = "#00FF00", title = "Green"), + ), + ), + default = listOf("#FF0000"), + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Colors", + "minItems": 1, + "maxItems": 2, + "items": { + "anyOf": [ + {"const": "#FF0000", "title": "Red"}, + {"const": "#00FF00", "title": "Green"} + ] + }, + "default": ["#FF0000"], + "type": "array" + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize TitledMultiSelectEnumSchema from JSON`() { + val json = """ + { + "type": "array", + "items": { + "anyOf": [ + {"const": "x", "title": "X"}, + {"const": "y", "title": "Y"} + ] + } + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.items.anyOf shouldHaveSize 2 + schema.items.anyOf[0].const shouldBe "x" + } + + // ── LegacyTitledEnumSchema ──────────────────────────────────────────── + + @Suppress("DEPRECATION") + @Test + fun `should serialize LegacyTitledEnumSchema`() { + verifySerialization( + LegacyTitledEnumSchema( + title = "Status", + enumValues = listOf("opt1", "opt2", "opt3"), + enumNames = listOf("Option One", "Option Two", "Option Three"), + default = "opt1", + ) as PrimitiveSchemaDefinition, + McpJson, + """ + { + "title": "Status", + "enum": ["opt1", "opt2", "opt3"], + "enumNames": ["Option One", "Option Two", "Option Three"], + "default": "opt1", + "type": "string" + } + """.trimIndent(), + ) + } + + @Suppress("DEPRECATION") + @Test + fun `should deserialize LegacyTitledEnumSchema from JSON`() { + val json = """ + { + "type": "string", + "enum": ["a", "b"], + "enumNames": ["Alpha", "Beta"] + } + """.trimIndent() + + val schema = verifyDeserialization(McpJson, json) + schema.shouldBeInstanceOf() + schema.enumValues shouldBe listOf("a", "b") + schema.enumNames shouldBe listOf("Alpha", "Beta") + } + + // ── Error cases ───────────────────────────────────────────────────── + + @Test + fun `should throw on unknown type`() { + val json = """{"type": "unknown"}""" + shouldThrow { + McpJson.decodeFromString(json) + } + } + + // ── RequestedSchema with typed properties ─────────────────────────── + + @Test + fun `should round-trip RequestedSchema with mixed property types`() { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "name" to StringSchema(title = "Name"), + "age" to IntegerSchema(minimum = 0), + "confirmed" to BooleanSchema(default = false), + "color" to UntitledSingleSelectEnumSchema(enumValues = listOf("Red", "Blue")), + ), + required = listOf("name"), + ) + + verifySerialization( + schema, + McpJson, + """ + { + "properties": { + "name": {"title": "Name", "type": "string"}, + "age": {"minimum": 0, "type": "integer"}, + "confirmed": {"default": false, "type": "boolean"}, + "color": {"enum": ["Red", "Blue"], "type": "string"} + }, + "required": ["name"], + "type": "object" + } + """.trimIndent(), + ) + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ElicitationDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ElicitationDslTest.kt index e49e4d2ba..4a34bab67 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ElicitationDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ElicitationDslTest.kt @@ -2,8 +2,11 @@ package io.modelcontextprotocol.kotlin.sdk.types.dsl import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema import io.modelcontextprotocol.kotlin.sdk.types.buildElicitRequest import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put @@ -24,14 +27,15 @@ class ElicitationDslTest { } request.params.message shouldBe "Provide info" - request.params.requestedSchema.properties["email"] shouldBe buildJsonObject { put("type", "string") } - request.params.requestedSchema.required shouldBe listOf("email") + val formParams = request.params.shouldBeInstanceOf() + formParams.requestedSchema.properties["email"].shouldBeInstanceOf() + formParams.requestedSchema.required shouldBe listOf("email") } @Test fun `buildElicitRequest should support direct requestedSchema`() { val schema = ElicitRequestParams.RequestedSchema( - properties = buildJsonObject { put("name", buildJsonObject { put("type", "string") }) }, + properties = mapOf("name" to StringSchema()), required = listOf("name"), ) val request = buildElicitRequest { @@ -39,19 +43,20 @@ class ElicitationDslTest { requestedSchema(schema) } - request.params.requestedSchema shouldBe schema + val formParams = request.params.shouldBeInstanceOf() + formParams.requestedSchema shouldBe schema } @Test fun `ElicitRequestedSchemaBuilder should support direct properties assignment`() { - val props = buildJsonObject { put("key", "value") } val request = buildElicitRequest { message = "Test" requestedSchema { - properties(props) + properties(buildJsonObject { put("key", buildJsonObject { put("type", "string") }) }) } } - request.params.requestedSchema.properties shouldBe props + val formParams = request.params.shouldBeInstanceOf() + formParams.requestedSchema.properties["key"].shouldBeInstanceOf() } @Test diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index fef34b261..027af1c76 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -1,8 +1,10 @@ public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/ClientConnection { public abstract fun createElicitation (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun createElicitation (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun createElicitation (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public abstract fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public abstract fun getSessionId ()Ljava/lang/String; @@ -22,6 +24,7 @@ public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/Client public final class io/modelcontextprotocol/kotlin/sdk/server/ClientConnection$DefaultImpls { public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun notification$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ServerNotification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; @@ -108,7 +111,9 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { public final fun clientConnection (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection; public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun createElicitation (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun createElicitation (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;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;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun createMessage (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/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;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun createSession (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -159,7 +164,9 @@ public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelc protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V public final fun createElicitation (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun createElicitation (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/ServerSession;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public fun equals (Ljava/lang/Object;)Z diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt index 998d62c44..96177eede 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt @@ -5,7 +5,9 @@ import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest @@ -104,6 +106,24 @@ public interface ClientConnection { options: RequestOptions? = null, ): ElicitResult + /** + * Sends a URL mode elicitation request to the client, directing the user + * to an external URL for out-of-band interactions. + * + * @param message The message explaining why the interaction is needed. + * @param elicitationId A unique identifier for the elicitation. + * @param url The URL that the user should navigate to. + * @param options Optional request options. + * @return The result of the elicitation request. + * @throws IllegalStateException If the server or client does not support elicitation. + */ + public suspend fun createElicitation( + message: String, + elicitationId: String, + url: String, + options: RequestOptions? = null, + ): ElicitResult + /** * Sends a message to the client requesting elicitation. * This typically results in a form being displayed to the user. @@ -189,10 +209,16 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client } override suspend fun createElicitation(request: ElicitRequest, options: RequestOptions?): ElicitResult { - with(request.params) { - logger.debug { - "Creating elicitation with message length=${message.length}, " + - "schema properties count=${requestedSchema.properties.size}" + val params = request.params + logger.debug { + when (params) { + is ElicitRequestFormParams -> + "Creating form elicitation with message length=${params.message.length}, " + + "schema properties count=${params.requestedSchema.properties.size}" + + is ElicitRequestURLParams -> + "Creating URL elicitation with message length=${params.message.length}, " + + "elicitationId=${params.elicitationId}" } } logger.trace { "ElicitRequest: $request" } @@ -204,7 +230,19 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client requestedSchema: ElicitRequestParams.RequestedSchema, options: RequestOptions?, ): ElicitResult = createElicitation( - request = ElicitRequest(params = ElicitRequestParams(message, requestedSchema)), + request = ElicitRequest(params = ElicitRequestFormParams(message = message, requestedSchema = requestedSchema)), + options = options, + ) + + override suspend fun createElicitation( + message: String, + elicitationId: String, + url: String, + options: RequestOptions?, + ): ElicitResult = createElicitation( + request = ElicitRequest( + params = ElicitRequestURLParams(message = message, elicitationId = elicitationId, url = url), + ), options = options, ) diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index e566d099f..a3b225667 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -780,6 +780,30 @@ public open class Server( options = options, ) + /** + * Triggers URL mode [ClientConnection.createElicitation] request for session by provided [sessionId]. + * + * @param sessionId The session ID to create elicitation for. + * @param message The message explaining why the interaction is needed. + * @param elicitationId A unique identifier for the elicitation. + * @param url The URL that the user should navigate to. + * @param options Optional request options. + * @return The created elicitation result. + * @throws IllegalStateException If the server does not support elicitation or if the request fails. + */ + public suspend fun createElicitation( + sessionId: String, + message: String, + elicitationId: String, + url: String, + options: RequestOptions? = null, + ): ElicitResult = clientConnection(sessionId).createElicitation( + message = message, + elicitationId = elicitationId, + url = url, + options = options, + ) + /** * Triggers [ClientConnection.sendLoggingMessage] for session by provided [sessionId]. * diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt index a8a34aff3..9e6553a0e 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -382,6 +382,24 @@ public open class ServerSession( options: RequestOptions? = null, ): ElicitResult = clientConnection.createElicitation(message, requestedSchema, options) + /** + * Sends a URL mode elicitation request to the client, directing the user + * to an external URL for out-of-band interactions. + * + * @param message The message explaining why the interaction is needed. + * @param elicitationId A unique identifier for the elicitation. + * @param url The URL that the user should navigate to. + * @param options Optional request options. + * @return The result of the elicitation request. + * @throws IllegalStateException If the server or client does not support elicitation. + */ + public suspend fun createElicitation( + message: String, + elicitationId: String, + url: String, + options: RequestOptions? = null, + ): ElicitResult = clientConnection.createElicitation(message, elicitationId, url, options) + /** * Sends a logging message notification to the client. * Messages are filtered based on the current logging level set by the client. From 6ce9ff81e7cc5fe9e27e654f32f98dce7ca965b0 Mon Sep 17 00:00:00 2001 From: johny <69754601+jskjw157@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:57:17 +0900 Subject: [PATCH 061/187] feat(server): support configurable max request payload size (#646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Add `maxRequestBodySize` parameter to `StreamableHttpServerTransport.Configuration`, allowing consumers to set a custom request body size limit lower than the default 4 MB. - Add `maxRequestBodySize: Long` property to `Configuration` with a default of 4 MB (`4,194,304` bytes) - Replace hardcoded `MAXIMUM_MESSAGE_SIZE` constant with the configurable value in `parseBody()` - Use `Long` for content-length comparison to avoid `Int` overflow on large values ## Motivation and Context Closes #521 The `StreamableHttpServerTransport` has a hardcoded 4 MB request body limit. Consumers who need a lower limit currently have to manually count the request payload size before passing it to the SDK. This change makes the limit configurable via `Configuration`. ## How Has This Been Tested? - Existing unit tests continue to pass (default 4 MB behavior unchanged) - Added `POST with custom max request body size rejects oversized payload` — verifies `PayloadTooLarge` is returned for payloads exceeding the custom limit - Added `POST at exactly custom max request body size is not rejected as payload too large` — verifies payloads at exactly the limit are not rejected by the size check ```bash ./gradlew :kotlin-sdk-server:jvmTest --tests "*.StreamableHttpServerTransportTest" ``` ## Breaking Changes None. The new parameter has a default value matching the previous hardcoded limit. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- kotlin-sdk-server/api/kotlin-sdk-server.api | 5 +- kotlin-sdk-server/detekt-baseline-main.xml | 1 + .../server/StreamableHttpServerTransport.kt | 27 +++++++--- .../StreamableHttpServerTransportTest.kt | 50 +++++++++++++++++++ 4 files changed, 73 insertions(+), 10 deletions(-) diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index 027af1c76..70e8e1401 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -227,13 +227,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServe } public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport$Configuration { - public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/time/Duration;JLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getAllowedHosts ()Ljava/util/List; public final fun getAllowedOrigins ()Ljava/util/List; public final fun getEnableDnsRebindingProtection ()Z public final fun getEnableJsonResponse ()Z public final fun getEventStore ()Lio/modelcontextprotocol/kotlin/sdk/server/EventStore; + public final fun getMaxRequestBodySize ()J public final fun getRetryInterval-FghU774 ()Lkotlin/time/Duration; } diff --git a/kotlin-sdk-server/detekt-baseline-main.xml b/kotlin-sdk-server/detekt-baseline-main.xml index 141c7d086..a27f38a22 100644 --- a/kotlin-sdk-server/detekt-baseline-main.xml +++ b/kotlin-sdk-server/detekt-baseline-main.xml @@ -4,6 +4,7 @@ InjectDispatcher:FeatureNotificationService.kt:FeatureNotificationService$Default LongParameterList:KtorServer.kt:private suspend fun RoutingContext.streamableTransport: StreamableHttpServerTransport? + LongParameterList:StreamableHttpServerTransport.kt:StreamableHttpServerTransport.Configuration MagicNumber:StdioServerTransport.kt:StdioServerTransport$8192 MaxLineLength:SSEServerTransport.kt:SseServerTransport$"SSEServerTransport already started! If using Server class, note that connect() calls start() automatically." MaxLineLength:SSEServerTransport.kt:SseServerTransport$* diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 821c525a1..1ea8d83d0 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -45,7 +45,7 @@ import kotlin.uuid.Uuid internal const val MCP_SESSION_ID_HEADER = "mcp-session-id" private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID" -private const val MAXIMUM_MESSAGE_SIZE = 4 * 1024 * 1024 // 4 MB +private const val DEFAULT_MAX_REQUEST_BODY_SIZE: Long = 4L * 1024 * 1024 // 4 MB private const val MIN_PRIMING_EVENT_PROTOCOL_VERSION = "2025-11-25" /** @@ -141,6 +141,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat * * @property retryInterval Retry interval for event handling or reconnection attempts. * Defaults to `null`. + * + * @property maxRequestBodySize Maximum allowed size (in bytes) for incoming request bodies. + * Defaults to 4 MB (4,194,304 bytes). */ public class Configuration( public val enableJsonResponse: Boolean = false, @@ -149,7 +152,14 @@ public class StreamableHttpServerTransport(private val configuration: Configurat public val allowedOrigins: List? = null, public val eventStore: EventStore? = null, public val retryInterval: Duration? = null, - ) + public val maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE, + ) { + init { + require(maxRequestBodySize > 0) { + "maxRequestBodySize must be greater than 0" + } + } + } public var sessionId: String? = null private set @@ -661,24 +671,25 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("ReturnCount", "MagicNumber") + @Suppress("ReturnCount") private suspend fun parseBody(call: ApplicationCall): List? { - val contentLength = call.request.header(HttpHeaders.ContentLength)?.toIntOrNull() ?: 0 - if (contentLength > MAXIMUM_MESSAGE_SIZE) { + val maxSize = configuration.maxRequestBodySize + val contentLength = call.request.header(HttpHeaders.ContentLength)?.toLongOrNull() ?: 0L + if (contentLength > maxSize) { call.reject( HttpStatusCode.PayloadTooLarge, RPCError.ErrorCode.INVALID_REQUEST, - "Invalid Request: message size exceeds maximum of ${MAXIMUM_MESSAGE_SIZE / (1024 * 1024)} MB", + "Invalid Request: message size exceeds maximum of $maxSize bytes", ) return null } val body = call.receiveText() - if (body.length > MAXIMUM_MESSAGE_SIZE) { + if (body.length.toLong() > maxSize) { call.reject( HttpStatusCode.PayloadTooLarge, RPCError.ErrorCode.INVALID_REQUEST, - "Invalid Request: message size exceeds maximum of ${MAXIMUM_MESSAGE_SIZE / (1024 * 1024)} MB", + "Invalid Request: message size exceeds maximum of $maxSize bytes", ) return null } diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt index c6e539adb..9cf916afb 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt @@ -43,10 +43,12 @@ import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNotNull import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation @@ -63,6 +65,15 @@ class StreamableHttpServerTransportTest { null, "lolol", ) + + private val sizeTestPayload = "x".repeat(64) + + @JvmStatic + fun maxBodySizeTestCases(): List = listOf( + Arguments.of(sizeTestPayload.length.toLong() - 1, HttpStatusCode.PayloadTooLarge), + Arguments.of(sizeTestPayload.length.toLong(), HttpStatusCode.BadRequest), + Arguments.of(sizeTestPayload.length.toLong() + 1, HttpStatusCode.BadRequest), + ) } private val path = "/transport" @@ -383,6 +394,45 @@ class StreamableHttpServerTransportTest { response.status shouldBe HttpStatusCode.PayloadTooLarge } + @ParameterizedTest + @MethodSource("maxBodySizeTestCases") + fun `POST with custom max request body size validates payload size`( + maxSize: Long, + expectedStatus: HttpStatusCode, + ) = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration( + enableJsonResponse = true, + maxRequestBodySize = maxSize, + ), + ) + transport.onMessage { message -> + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + + configureTransportEndpoint(transport) + + val response = client.post(path) { + addStreamableHeaders() + setBody(sizeTestPayload) + } + + response.status shouldBe expectedStatus + } + + @Test + fun `Configuration with negative maxRequestBodySize throws IllegalArgumentException`() { + assertFailsWith { + StreamableHttpServerTransport.Configuration(maxRequestBodySize = -1) + } + } + private fun ApplicationTestBuilder.configureTransportEndpoint(transport: StreamableHttpServerTransport) { application { routing { From 9c4a2394b40e89f3d8e560d7a8ae0c4a5a3a6d5b Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 1 Apr 2026 15:22:35 +0200 Subject: [PATCH 062/187] test: remove `Ignore` for SseTransportTest and refactor (#662) SseTransportTest passes on wasm after updating ktor ## How Has This Been Tested? jvm/js/wasm/macos ## Breaking Changes none ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- .../kotlin/sdk/client/SseTransportTest.kt | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt index d82ff47a6..8abf75d83 100644 --- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseTransportTest.kt @@ -12,9 +12,10 @@ import io.modelcontextprotocol.kotlin.sdk.server.mcp import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext 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 @@ -37,7 +38,6 @@ class SseTransportTest : BaseTransportTest() { } @Test - @Ignore // Ignored because it doesn’t work with wasm/js in Ktor 3.2.3 fun `should start then close cleanly`() = runTest { val server = embeddedServer(CIO, port = 0) { install(ServerSSE) @@ -58,13 +58,12 @@ class SseTransportTest : BaseTransportTest() { } try { - testTransportRead(transport) + testTransportOpenClose(transport) } finally { server.stopSuspend() } } - @Ignore @Test fun `should read messages`() = runTest { val server = embeddedServer(CIO, port = 0) { @@ -85,20 +84,28 @@ class SseTransportTest : BaseTransportTest() { } } + val client = Client( + clientInfo = Implementation(name = "test-client", version = "1.0"), + options = ClientOptions(), + ) + try { - testTransportRead(transport) + withContext(Dispatchers.Default) { + client.connect(transport) + client.ping() + } } finally { + client.close() server.stopSuspend() } } - @Ignore @Test fun `test sse path not root path`() = runTest { val server = embeddedServer(CIO, port = 0) { install(ServerSSE) routing { - mcp { mcpServer } + mcp("/sse") { mcpServer } } }.startSuspend(wait = false) @@ -114,9 +121,18 @@ class SseTransportTest : BaseTransportTest() { } } + val client = Client( + clientInfo = Implementation(name = "test-client", version = "1.0"), + options = ClientOptions(), + ) + try { - testTransportRead(transport) + withContext(Dispatchers.Default) { + client.connect(transport) + client.ping() + } } finally { + client.close() server.stopSuspend() } } From e21d2298a1a60c735c1532d46e926fc1309bdcd5 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 1 Apr 2026 16:31:34 +0200 Subject: [PATCH 063/187] fix: apply elicitation schema defaults for missing fields in client response (#661) Apply default values from the elicitation schema to the client's response when the handler returns `Accept` with missing fields closes #414 ## How Has This Been Tested? integration and conformance tests ## Breaking Changes none ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- conformance-test/conformance-baseline.yml | 3 +- .../kotlin/sdk/client/ClientTest.kt | 180 ++++++++++++++++++ .../kotlin/sdk/client/Client.kt | 68 ++++++- 3 files changed, 248 insertions(+), 3 deletions(-) diff --git a/conformance-test/conformance-baseline.yml b/conformance-test/conformance-baseline.yml index 337d8ce61..f349b8c75 100644 --- a/conformance-test/conformance-baseline.yml +++ b/conformance-test/conformance-baseline.yml @@ -2,5 +2,4 @@ # Add entries here as tests are identified as known SDK limitations server: [] -client: - - elicitation-sep1034-client-defaults +client: [] diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index 1d84732a2..d0acda59d 100644 --- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -7,9 +7,11 @@ import io.modelcontextprotocol.kotlin.sdk.server.ServerSession import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult +import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult @@ -17,6 +19,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult +import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse @@ -38,8 +41,11 @@ import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.StringSchema import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.TitledMultiSelectEnumSchema import io.modelcontextprotocol.kotlin.sdk.types.Tool import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel @@ -48,7 +54,10 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlin.coroutines.cancellation.CancellationException import kotlin.test.Test @@ -1110,4 +1119,175 @@ class ClientTest { assertEquals(elicitationResultAction, result.action) assertEquals(elicitationResultContent, result.content) } + + @Test + fun `should apply elicitation defaults for missing fields in empty content`() = runTest { + val schema = defaultsTestSchema() + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = JsonObject(emptyMap())) + } + + val result = serverSession.createElicitation(message = "fill defaults", requestedSchema = schema) + + assertEquals(ElicitResult.Action.Accept, result.action) + val content = result.content!! + assertEquals(JsonPrimitive("John Doe"), content["name"]) + assertEquals(JsonPrimitive(30), content["age"]) + assertEquals(JsonPrimitive(95.5), content["score"]) + assertEquals(JsonPrimitive("active"), content["status"]) + assertEquals(JsonPrimitive(true), content["verified"]) + + client.close() + } + + @Test + fun `should apply elicitation defaults only for missing fields preserving user values`() = runTest { + val schema = defaultsTestSchema() + val userContent = buildJsonObject { put("name", "Custom Name") } + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = userContent) + } + + val result = serverSession.createElicitation(message = "partial", requestedSchema = schema) + + val content = result.content!! + assertEquals(JsonPrimitive("Custom Name"), content["name"]) + assertEquals(JsonPrimitive(30), content["age"]) + assertEquals(JsonPrimitive(95.5), content["score"]) + assertEquals(JsonPrimitive("active"), content["status"]) + assertEquals(JsonPrimitive(true), content["verified"]) + + client.close() + } + + @Test + fun `should not apply elicitation defaults when action is decline`() = runTest { + val schema = defaultsTestSchema() + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Decline) + } + + val result = serverSession.createElicitation(message = "decline", requestedSchema = schema) + + assertEquals(ElicitResult.Action.Decline, result.action) + assertNull(result.content) + + client.close() + } + + @Test + fun `should not apply elicitation defaults when action is cancel`() = runTest { + val schema = defaultsTestSchema() + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Cancel) + } + + val result = serverSession.createElicitation(message = "cancel", requestedSchema = schema) + + assertEquals(ElicitResult.Action.Cancel, result.action) + assertNull(result.content) + + client.close() + } + + @Test + fun `should not modify content when schema has no defaults`() = runTest { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "name" to StringSchema(description = "name"), + "age" to IntegerSchema(description = "age"), + ), + ) + val emptyContent = JsonObject(emptyMap()) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = emptyContent) + } + + val result = serverSession.createElicitation(message = "no defaults", requestedSchema = schema) + + assertEquals(ElicitResult.Action.Accept, result.action) + assertTrue(result.content!!.isEmpty()) + + client.close() + } + + @Test + fun `should apply elicitation defaults for multi-select enum schemas`() = runTest { + val schema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "tags" to UntitledMultiSelectEnumSchema( + description = "tags", + items = UntitledMultiSelectEnumSchema.Items(enumValues = listOf("a", "b", "c")), + default = listOf("a", "b"), + ), + "options" to TitledMultiSelectEnumSchema( + description = "options", + items = TitledMultiSelectEnumSchema.Items( + anyOf = listOf( + io.modelcontextprotocol.kotlin.sdk.types.EnumOption("x", "X"), + io.modelcontextprotocol.kotlin.sdk.types.EnumOption("y", "Y"), + ), + ), + default = listOf("x"), + ), + ), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = JsonObject(emptyMap())) + } + + val result = serverSession.createElicitation(message = "multi-select", requestedSchema = schema) + + val content = result.content!! + val tags = content["tags"]!! + assertIs(tags) + assertEquals(2, tags.size) + assertEquals("a", tags[0].jsonPrimitive.content) + assertEquals("b", tags[1].jsonPrimitive.content) + + val options = content["options"]!! + assertIs(options) + assertEquals(1, options.size) + assertEquals("x", options[0].jsonPrimitive.content) + + client.close() + } + + private fun defaultsTestSchema(): ElicitRequestParams.RequestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf( + "name" to StringSchema(description = "User name", default = "John Doe"), + "age" to IntegerSchema(description = "User age", default = 30), + "score" to DoubleSchema(description = "User score", default = 95.5), + "status" to UntitledSingleSelectEnumSchema( + description = "User status", + enumValues = listOf("active", "inactive", "pending"), + default = "active", + ), + "verified" to BooleanSchema(description = "Verification status", default = true), + ), + ) + + private suspend fun setupElicitationPair( + handler: (io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest) -> ElicitResult, + ): Pair = kotlinx.coroutines.coroutineScope { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions(capabilities = ClientCapabilities(elicitation = EmptyJsonObject)), + ) + client.setElicitationHandler(handler) + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) + + val serverSessionResult = CompletableDeferred() + listOf( + launch { client.connect(clientTransport) }, + launch { serverSessionResult.complete(server.createSession(serverTransport)) }, + ).joinAll() + + client to serverSessionResult.await() + } } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index e3addedae..34b9a5bf3 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -6,13 +6,16 @@ import io.modelcontextprotocol.kotlin.sdk.shared.Protocol import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest import io.modelcontextprotocol.kotlin.sdk.types.CompleteResult +import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest @@ -22,7 +25,9 @@ import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.LegacyTitledEnumSchema import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsRequest import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest @@ -37,6 +42,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel import io.modelcontextprotocol.kotlin.sdk.types.McpException import io.modelcontextprotocol.kotlin.sdk.types.Method import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.PrimitiveSchemaDefinition import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult import io.modelcontextprotocol.kotlin.sdk.types.RequestMeta @@ -46,8 +52,13 @@ import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest import io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.TitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.TitledSingleSelectEnumSchema import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema import io.modelcontextprotocol.kotlin.sdk.types.toJson import kotlinx.atomicfu.atomic import kotlinx.atomicfu.getAndUpdate @@ -56,7 +67,10 @@ import kotlinx.collections.immutable.minus import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toPersistentSet import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray import kotlin.coroutines.cancellation.CancellationException private val logger = KotlinLogging.logger {} @@ -576,6 +590,10 @@ public open class Client(private val clientInfo: Implementation, options: Client /** * Sets the elicitation handler. * + * When the handler returns [ElicitResult.Action.Accept], any properties missing from + * [ElicitResult.content] are automatically populated with default values defined in the + * elicitation schema. + * * @param handler The elicitation handler. * @throws IllegalStateException if the client does not support elicitation. */ @@ -587,12 +605,60 @@ public open class Client(private val clientInfo: Implementation, options: Client logger.info { "Setting the elicitation handler" } setRequestHandler(Method.Defined.ElicitationCreate) { request, _ -> - handler(request) + val result = handler(request) + applyElicitationDefaults(request, result) } } // --- Internal Handlers --- + @Suppress("ReturnCount") + private fun applyElicitationDefaults(request: ElicitRequest, result: ElicitResult): ElicitResult { + if (result.action != ElicitResult.Action.Accept) return result + val formParams = request.params as? ElicitRequestFormParams ?: return result + val content = result.content ?: return result + + val merged = buildMap { + putAll(content) + for ((key, schemaDef) in formParams.requestedSchema.properties) { + if (key !in content) { + schemaDef.defaultJsonValue()?.let { put(key, it) } + } + } + } + + return if (merged.size == content.size) { + result + } else { + result.copy(content = JsonObject(merged)) + } + } + + @Suppress("DEPRECATION", "CyclomaticComplexMethod") + private fun PrimitiveSchemaDefinition.defaultJsonValue(): JsonElement? = when (this) { + is StringSchema -> default?.let { JsonPrimitive(it) } + + is IntegerSchema -> default?.let { JsonPrimitive(it) } + + is DoubleSchema -> default?.let { JsonPrimitive(it) } + + is BooleanSchema -> default?.let { JsonPrimitive(it) } + + is UntitledSingleSelectEnumSchema -> default?.let { JsonPrimitive(it) } + + is TitledSingleSelectEnumSchema -> default?.let { JsonPrimitive(it) } + + is LegacyTitledEnumSchema -> default?.let { JsonPrimitive(it) } + + is UntitledMultiSelectEnumSchema -> default?.let { list -> + buildJsonArray { list.forEach { add(JsonPrimitive(it)) } } + } + + is TitledMultiSelectEnumSchema -> default?.let { list -> + buildJsonArray { list.forEach { add(JsonPrimitive(it)) } } + } + } + private fun handleListRoots(): ListRootsResult { val rootList = roots.value.values.toList() return ListRootsResult(rootList) From 3617ce4e158fa61c1f24733036bab0f624f6c3f1 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:01:13 +0300 Subject: [PATCH 064/187] feat(server): auto-install ContentNegotiation with McpJson (#664) (#665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users currently must manually `install(ContentNegotiation) { json(McpJson) }` before calling `mcp()`, `mcpStreamableHttp()`, or `mcpStatelessStreamableHttp()`. Forgetting this or using the default `json()` instead of `json(McpJson)` causes silent serialization failures at runtime (e.g., explicit null fields in JSON-RPC responses). This PR makes the SDK install it automatically. Fixes #664 ## Changes - **Core feature** — `mcp()`, `mcpStreamableHttp()`, and `mcpStatelessStreamableHttp()` now auto-install `ContentNegotiation` configured with `McpJson`. If the user already installed `ContentNegotiation`, a warning is logged instead of crashing with `DuplicatePluginException`. - **`KtorServerHelpers.kt`** (new) — `installMcpContentNegotiation()` internal helper with `pluginOrNull` guard and warning - **`KtorServer.kt`** — calls the helper from all three entry points - **`build.gradle.kts`** — adds `ktor-server-content-negotiation` and `ktor-serialization` as `implementation` deps to `kotlin-sdk-server` - **Removed manual installs** from `ConformanceServer.kt` and `AbstractStreamableHttpIntegrationTest.kt`. - **Test infrastructure** — replaced `slf4j-simple` with Logback and use `ListAppender` (`LogCapture` utility) for deterministic log assertion. Added `logback-test.xml` matching previous log levels. - **`StreamableHttpServerConfigurationTest.kt`** (new) — verifies the warning for all three entry points: `mcpStreamableHttp`, `mcpStatelessStreamableHttp`, and `mcp` (SSE). - **README.md** — removed manual `ContentNegotiation` boilerplate from examples, documented automatic installation, added CORS configuration snippet for browser-based clients. - **CI** — added `ktlintCheck` to the Linux build matrix job. ## How Has This Been Tested? Unit/integration tests. Conformance tests updated ## Breaking Changes No ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .github/workflows/build.yml | 2 +- README.md | 87 ++++++++------- .../sdk/conformance/ConformanceServer.kt | 7 -- gradle/libs.versions.toml | 2 + .../AbstractStreamableHttpIntegrationTest.kt | 7 -- kotlin-sdk-server/build.gradle.kts | 4 +- .../kotlin/sdk/server/KtorServer.kt | 12 ++ .../kotlin/sdk/server/KtorServerHelpers.kt | 46 ++++++++ .../kotlin/sdk/server/LogCapture.kt | 35 ++++++ .../StreamableHttpServerConfigurationTest.kt | 103 ++++++++++++++++++ .../src/jvmTest/resources/logback-test.xml | 15 +++ .../jvmTest/resources/simplelogger.properties | 8 -- 12 files changed, 267 insertions(+), 61 deletions(-) create mode 100644 kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServerHelpers.kt create mode 100644 kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/LogCapture.kt create mode 100644 kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerConfigurationTest.kt create mode 100644 kotlin-sdk-server/src/jvmTest/resources/logback-test.xml delete mode 100644 kotlin-sdk-server/src/jvmTest/resources/simplelogger.properties diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3467db243..08242f769 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: include: - os: ubuntu-latest job-name: "Linux Tests" - gradle-tasks: "build" + gradle-tasks: "ktlintCheck build" max-workers: 3 - os: macos-latest diff --git a/README.md b/README.md index 3e65b26a4..58a2cadf7 100644 --- a/README.md +++ b/README.md @@ -19,34 +19,34 @@ standardized protocol interface. * [Overview](#overview) * [Installation](#installation) - * [Artifacts](#artifacts) - * [Gradle setup (JVM)](#gradle-setup-jvm) - * [Multiplatform](#multiplatform) - * [Ktor dependencies](#ktor-dependencies) + * [Artifacts](#artifacts) + * [Gradle setup (JVM)](#gradle-setup-jvm) + * [Multiplatform](#multiplatform) + * [Ktor dependencies](#ktor-dependencies) * [Quickstart](#quickstart) - * [Creating a Client](#creating-a-client) - * [Creating a Server](#creating-a-server) + * [Creating a Client](#creating-a-client) + * [Creating a Server](#creating-a-server) * [Core Concepts](#core-concepts) - * [MCP Primitives](#mcp-primitives) - * [Capabilities](#capabilities) - * [Server Capabilities](#server-capabilities) - * [Client Capabilities](#client-capabilities) - * [Server Features](#server-features) - * [Prompts](#prompts) - * [Resources](#resources) - * [Tools](#tools) - * [Completion](#completion) - * [Logging](#logging) - * [Pagination](#pagination) - * [Client Features](#client-features) - * [Roots](#roots) - * [Sampling](#sampling) + * [MCP Primitives](#mcp-primitives) + * [Capabilities](#capabilities) + * [Server Capabilities](#server-capabilities) + * [Client Capabilities](#client-capabilities) + * [Server Features](#server-features) + * [Prompts](#prompts) + * [Resources](#resources) + * [Tools](#tools) + * [Completion](#completion) + * [Logging](#logging) + * [Pagination](#pagination) + * [Client Features](#client-features) + * [Roots](#roots) + * [Sampling](#sampling) * [Transports](#transports) - * [STDIO Transport](#stdio-transport) - * [Streamable HTTP Transport](#streamable-http-transport) - * [SSE Transport](#sse-transport) - * [WebSocket Transport](#websocket-transport) - * [ChannelTransport (testing)](#channeltransport-testing) + * [STDIO Transport](#stdio-transport) + * [Streamable HTTP Transport](#streamable-http-transport) + * [SSE Transport](#sse-transport) + * [WebSocket Transport](#websocket-transport) + * [ChannelTransport (testing)](#channeltransport-testing) * [Connecting your server](#connecting-your-server) * [Examples](#examples) * [Documentation](#documentation) @@ -188,24 +188,19 @@ the [simple-streamable-server](samples/simple-streamable-server) sample. ```kotlin -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.install import io.ktor.server.cio.CIO import io.ktor.server.engine.embeddedServer -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation -import io.modelcontextprotocol.kotlin.sdk.types.McpJson import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put - fun main(args: Array) { val port = args.firstOrNull()?.toIntOrNull() ?: 3000 val mcpServer = Server( @@ -231,11 +226,8 @@ fun main(args: Array) { ) { request -> CallToolResult(content = listOf(TextContent("Hello, world!"))) } - + embeddedServer(CIO, host = "127.0.0.1", port = port) { - install(ContentNegotiation) { - json(McpJson) - } mcpStreamableHttp { mcpServer } @@ -774,8 +766,10 @@ CLI tooling that spawns a helper process. No networking setup is required. `StreamableHttpClientTransport` and the Ktor `mcpStreamableHttp()` / `mcpStatelessStreamableHttp()` helpers expose MCP over a single HTTP endpoint with optional JSON-only or SSE streaming responses. This is the recommended choice for -remote deployments and integrates nicely with proxies or service meshes. Both accept a `path` parameter (default: -`"/mcp"`) to mount the endpoint at any URL: +remote deployments and integrates nicely with proxies or service meshes. + +These helpers automatically install `ContentNegotiation` with `McpJson` — do not install it yourself, or a warning will +be logged. Both accept a `path` parameter (default: `"/mcp"`) to mount the endpoint at any URL: +**CORS for browser-based clients (e.g. MCP Inspector):** if you connect from a browser-based +client you need to install the Ktor CORS plugin so that MCP-specific headers are allowed and exposed: + +```kotlin +install(CORS) { + anyHost() // restrict to specific origins in production + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowMethod(HttpMethod.Delete) + allowNonSimpleContentTypes = true + allowHeader("Mcp-Session-Id") + allowHeader("Mcp-Protocol-Version") + exposeHeader("Mcp-Session-Id") + exposeHeader("Mcp-Protocol-Version") +} +``` + ### SSE Transport Server-Sent Events remain available for backwards compatibility with older MCP clients. Two Ktor helpers are provided: -- **`Application.mcp { }`** — installs the SSE plugin automatically and registers MCP endpoints at `/`. +- **`Application.mcp { }`** — installs SSE and `ContentNegotiation` with `McpJson` automatically, then registers MCP + endpoints at `/`. Do not install `ContentNegotiation` yourself — the SDK handles it. - **`Route.mcp { }`** — registers MCP endpoints at the current route path; requires `install(SSE)` in the application first. Use this to host MCP alongside other routes or under a path prefix: diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt index b42077529..03d5ee39e 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt @@ -1,23 +1,16 @@ package io.modelcontextprotocol.kotlin.sdk.conformance -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.install import io.ktor.server.cio.CIO import io.ktor.server.engine.embeddedServer -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp import io.modelcontextprotocol.kotlin.sdk.types.Implementation -import io.modelcontextprotocol.kotlin.sdk.types.McpJson import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities fun main() { val port = System.getenv("MCP_PORT")?.toIntOrNull() ?: 3001 embeddedServer(CIO, port = port) { - install(ContentNegotiation) { - json(McpJson) - } mcpStreamableHttp( enableDnsRebindingProtection = true, allowedHosts = listOf("localhost", "127.0.0.1", "localhost:$port", "127.0.0.1:$port"), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f511d0d56..78f55fe46 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ mockk = "1.14.9" mokksy = "0.8.1" serialization = "1.10.0" slf4j = "2.0.17" +logback = "1.5.32" junit = "5.14.3" [libraries] @@ -67,6 +68,7 @@ mockk = { module = "io.mockk:mockk", version.ref = "mockk" } mokksy = { group = "dev.mokksy", name = "mokksy", version.ref = "mokksy" } netty-bom = { group = "io.netty", name = "netty-bom", version.ref = "netty" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } +logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } # Samples diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt index 0f6646e5d..9bc0c1745 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt @@ -1,17 +1,13 @@ package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp -import io.ktor.serialization.kotlinx.json.json -import io.ktor.server.application.install import io.ktor.server.cio.CIOApplicationEngine import io.ktor.server.engine.EmbeddedServer import io.ktor.server.engine.embeddedServer -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation -import io.modelcontextprotocol.kotlin.sdk.types.McpJson import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage import io.modelcontextprotocol.kotlin.sdk.types.Role @@ -56,9 +52,6 @@ open class AbstractStreamableHttpIntegrationTest { host = URL, port = PORT, ) { - install(ContentNegotiation) { - json(McpJson) - } mcpStreamableHttp { mcpServer } } diff --git a/kotlin-sdk-server/build.gradle.kts b/kotlin-sdk-server/build.gradle.kts index 3a0a3985a..126c51601 100644 --- a/kotlin-sdk-server/build.gradle.kts +++ b/kotlin-sdk-server/build.gradle.kts @@ -12,7 +12,9 @@ kotlin { api(project(":kotlin-sdk-core")) api(libs.ktor.server.core) api(libs.ktor.server.sse) + implementation(libs.ktor.server.content.negotiation) implementation(libs.ktor.server.websockets) + implementation(libs.ktor.serialization) implementation(libs.kotlin.logging) } } @@ -37,7 +39,7 @@ kotlin { implementation(libs.ktor.server.content.negotiation) implementation(libs.ktor.server.test.host) - runtimeOnly(libs.slf4j.simple) + implementation(libs.logback.classic) } } } 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 739f8d3c1..35dcc7ecc 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 @@ -86,11 +86,15 @@ public fun Route.mcp(block: ServerSSESession.() -> Server) { * over [Server-Sent Events (SSE) Transport](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) * and sets up routing with the provided configuration block. * + * Automatically installs [ContentNegotiation][io.ktor.server.plugins.contentnegotiation.ContentNegotiation] + * with [McpJson][io.modelcontextprotocol.kotlin.sdk.types.McpJson] and [SSE]. + * * @param block factory block with access to the [ServerSSESession] * that creates and returns the [Server] to handle the connection. */ @KtorDsl public fun Application.mcp(block: ServerSSESession.() -> Server) { + installMcpContentNegotiation() install(SSE) routing { @@ -103,6 +107,7 @@ private fun Application.mcpStreamableHttp( configuration: StreamableHttpServerTransport.Configuration, block: RoutingContext.() -> Server, ) { + installMcpContentNegotiation() install(SSE) val transportManager = TransportManager() @@ -139,6 +144,9 @@ private fun Application.mcpStreamableHttp( * Sets up SSE, HTTP POST, and DELETE endpoints at the specified [path]. * Simple request/response pairs are returned as JSON (not SSE streams). * + * Automatically installs [ContentNegotiation][io.ktor.server.plugins.contentnegotiation.ContentNegotiation] + * with [McpJson][io.modelcontextprotocol.kotlin.sdk.types.McpJson] and [SSE]. + * * @param path The base path for the MCP Streamable HTTP endpoint. Defaults to "/mcp". * @param enableDnsRebindingProtection Enables DNS rebinding attack protection for the endpoint. Defaults to false. * @param allowedHosts A list of hostnames allowed to access the endpoint. If `null`, no restrictions are applied. @@ -177,6 +185,7 @@ private fun Application.mcpStatelessStreamableHttp( configuration: StreamableHttpServerTransport.Configuration, block: RoutingContext.() -> Server, ) { + installMcpContentNegotiation() install(SSE) routing { @@ -212,6 +221,9 @@ private fun Application.mcpStatelessStreamableHttp( * Sets up an HTTP POST endpoint at [path]. GET and DELETE requests return 405 Method Not Allowed. * Simple request/response pairs are returned as JSON (not SSE streams). * + * Automatically installs [ContentNegotiation][io.ktor.server.plugins.contentnegotiation.ContentNegotiation] + * with [McpJson][io.modelcontextprotocol.kotlin.sdk.types.McpJson] and [SSE]. + * * @param path The URL path where the server listens for incoming JSON-RPC requests. Defaults to "/mcp". * @param enableDnsRebindingProtection Determines whether DNS rebinding protection is enabled. Defaults to `false`. * @param allowedHosts A list of allowed hostnames. If null, host filtering is disabled. diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServerHelpers.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServerHelpers.kt new file mode 100644 index 000000000..42093c8b6 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServerHelpers.kt @@ -0,0 +1,46 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.application.pluginOrNull +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.util.AttributeKey +import io.modelcontextprotocol.kotlin.sdk.types.McpJson + +private val logger = KotlinLogging.logger {} + +private val McpContentNegotiationInstalled = AttributeKey("McpContentNegotiationInstalled") + +/** + * Installs [ContentNegotiation] configured with [McpJson]. + * + * MCP requires specific JSON settings (`explicitNulls = false`, `encodeDefaults = true`, + * `classDiscriminatorMode = NONE`). Using the default `json()` instead of `json(McpJson)` + * causes serialization failures at runtime (e.g., explicit null fields in JSON-RPC responses). + * + * If [ContentNegotiation] was already installed by user code, a warning is logged because + * Ktor's public API does not expose the Json configuration for verification. + * + * This function is idempotent: subsequent calls on the same [Application] return immediately + * without re-installing the plugin or logging additional warnings. + */ +internal fun Application.installMcpContentNegotiation() { + if (attributes.getOrNull(McpContentNegotiationInstalled) != null) return + attributes.put(McpContentNegotiationInstalled, Unit) + + if (pluginOrNull(ContentNegotiation) != null) { + // ContentNegotiation was already installed by user code. + logger.warn { + "ContentNegotiation is already installed. " + + "MCP requires json(McpJson) for correct JSON-RPC serialization " + + "(explicitNulls = false, encodeDefaults = true). " + + "Ensure your ContentNegotiation configuration includes json(McpJson)." + } + } else { + install(ContentNegotiation) { + json(McpJson) + } + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/LogCapture.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/LogCapture.kt new file mode 100644 index 000000000..dcf3ad23b --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/LogCapture.kt @@ -0,0 +1,35 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import org.slf4j.LoggerFactory + +/** + * Captures log events from the given logger (and its children) using a Logback [ListAppender]. + * + * Usage: + * ``` + * LogCapture("io.modelcontextprotocol.kotlin.sdk.server").use { logs -> + * // … code that emits log messages … + * logs.messages.shouldExist { "expected text" in it } + * } + * ``` + */ +internal class LogCapture(loggerName: String) : AutoCloseable { + private val logger: Logger = LoggerFactory.getLogger(loggerName) as Logger + private val appender: ListAppender = ListAppender() + + init { + appender.start() + logger.addAppender(appender) + } + + val messages: List + get() = appender.list.map { it.formattedMessage } + + override fun close() { + logger.detachAppender(appender) + appender.stop() + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerConfigurationTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerConfigurationTest.kt new file mode 100644 index 000000000..1efe2abc8 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerConfigurationTest.kt @@ -0,0 +1,103 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.collections.shouldExist +import io.kotest.matchers.collections.shouldHaveSize +import io.ktor.client.request.get +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.install +import io.ktor.server.testing.testApplication +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlin.test.Test +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation + +private const val MCP_SERVER_PACKAGE = "io.modelcontextprotocol.kotlin.sdk.server" + +class StreamableHttpServerConfigurationTest { + + private fun testServer(): Server = Server( + Implementation("test-server", "1.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + + @Test + fun `mcpStreamableHttp warns when ContentNegotiation is pre-installed`() { + LogCapture(MCP_SERVER_PACKAGE).use { logs -> + testApplication { + application { + install(ServerContentNegotiation) { + json() // Pre-install with default (wrong) config + } + mcpStreamableHttp { testServer() } + } + client.get("/mcp") + } + logs.messages.shouldExist { "ContentNegotiation is already installed" in it } + } + } + + @Test + fun `mcpStatelessStreamableHttp warns when ContentNegotiation is pre-installed`() { + LogCapture(MCP_SERVER_PACKAGE).use { logs -> + testApplication { + application { + install(ServerContentNegotiation) { + json() // Pre-install with default (wrong) config + } + mcpStatelessStreamableHttp { testServer() } + } + client.get("/mcp") + } + logs.messages.shouldExist { "ContentNegotiation is already installed" in it } + } + } + + @Test + fun `mcp warns when ContentNegotiation is pre-installed`() { + LogCapture(MCP_SERVER_PACKAGE).use { logs -> + testApplication { + application { + install(ServerContentNegotiation) { + json() // Pre-install with default (wrong) config + } + mcp { testServer() } + } + client.get("/sse") + } + logs.messages.shouldExist { "ContentNegotiation is already installed" in it } + } + } + + @Test + fun `installMcpContentNegotiation is idempotent`() { + LogCapture(MCP_SERVER_PACKAGE).use { logs -> + testApplication { + application { + installMcpContentNegotiation() + installMcpContentNegotiation() + } + client.get("/") + } + logs.messages.filter { "ContentNegotiation is already installed" in it } + .shouldHaveSize(0) + } + } + + @Test + fun `installMcpContentNegotiation warns exactly once when ContentNegotiation is pre-installed`() { + LogCapture(MCP_SERVER_PACKAGE).use { logs -> + testApplication { + application { + install(ServerContentNegotiation) { + json() + } + installMcpContentNegotiation() + installMcpContentNegotiation() + } + client.get("/") + } + logs.messages.filter { "ContentNegotiation is already installed" in it } + .shouldHaveSize(1) + } + } +} diff --git a/kotlin-sdk-server/src/jvmTest/resources/logback-test.xml b/kotlin-sdk-server/src/jvmTest/resources/logback-test.xml new file mode 100644 index 000000000..aa3347853 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + System.err + + [%thread] %level %logger - %msg%n + + + + + + + + + + diff --git a/kotlin-sdk-server/src/jvmTest/resources/simplelogger.properties b/kotlin-sdk-server/src/jvmTest/resources/simplelogger.properties deleted file mode 100644 index 95fb646f3..000000000 --- a/kotlin-sdk-server/src/jvmTest/resources/simplelogger.properties +++ /dev/null @@ -1,8 +0,0 @@ -# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) -org.slf4j.simpleLogger.defaultLogLevel=INFO -org.slf4j.simpleLogger.showThreadName=true -org.slf4j.simpleLogger.showDateTime=false - -# Log level for specific packages or classes -org.slf4j.simpleLogger.log.io.ktor.server=DEBUG -org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG From e5e5ec206fec96eee12013f1f886553914b8002d Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Thu, 2 Apr 2026 12:37:33 +0200 Subject: [PATCH 065/187] feat: add notification for elicitation complete (#667) closes #546 ## How Has This Been Tested? unit ## Breaking Changes none ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- .../kotlin/sdk/server/ClientConnectionTest.kt | 13 ++++ kotlin-sdk-core/api/kotlin-sdk-core.api | 60 +++++++++++++++++++ .../kotlin/sdk/types/methods.kt | 1 + .../kotlin/sdk/types/notification.kt | 30 ++++++++++ .../kotlin/sdk/types/serializers.kt | 1 + .../kotlin/sdk/types/NotificationTest.kt | 45 ++++++++++++++ kotlin-sdk-server/api/kotlin-sdk-server.api | 3 + .../kotlin/sdk/server/ClientConnection.kt | 13 ++++ .../kotlin/sdk/server/Server.kt | 11 ++++ .../kotlin/sdk/server/ServerSession.kt | 9 +++ 10 files changed, 186 insertions(+) diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt index 71f81297f..ee8953a7f 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt @@ -7,6 +7,8 @@ import io.kotest.matchers.string.shouldNotBeBlank import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotificationParams import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest @@ -77,6 +79,8 @@ class ClientConnectionTest : AbstractServerFeaturesTest() { val loggingMessage = onClientNotification(Method.Defined.NotificationsMessage) val resourceUpdated = onClientNotification(Method.Defined.NotificationsResourcesUpdated) + val elicitationComplete = + onClientNotification(Method.Defined.NotificationsElicitationComplete) init { onClientRequest(Method.Defined.RootsList) { @@ -109,6 +113,12 @@ class ClientConnectionTest : AbstractServerFeaturesTest() { withClue("sendResourceUpdated() delivered wrong URI") { resourceUri shouldBe "test://res" } + withClue("sendElicitationComplete() did not deliver a notification to the client") { + elicitationComplete.isCompleted shouldBe true + } + withClue("sendElicitationComplete() delivered wrong elicitationId") { + elicitationComplete.await().params.elicitationId shouldBe "elicit-123" + } capturedSessionId.shouldNotBeBlank() } } @@ -128,6 +138,9 @@ class ClientConnectionTest : AbstractServerFeaturesTest() { ), ) sendResourceUpdated(ResourceUpdatedNotification(ResourceUpdatedNotificationParams("test://res"))) + sendElicitationComplete( + ElicitationCompleteNotification(ElicitationCompleteNotificationParams(elicitationId = "elicit-123")), + ) cap.sessionId.complete(sessionId) } diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index 0b81586f9..d4f88ba47 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -1417,6 +1417,65 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitResult$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification : io/modelcontextprotocol/kotlin/sdk/types/ServerNotification { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification$Companion; + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification; + public fun equals (Ljava/lang/Object;)Z + public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams; + public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/NotificationParams; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification; + 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/types/ElicitationCompleteNotification;)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/types/ElicitationCompleteNotification$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams : io/modelcontextprotocol/kotlin/sdk/types/NotificationParams { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams$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/types/ElicitationCompleteNotificationParams; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams; + public fun equals (Ljava/lang/Object;)Z + public final fun getElicitationId ()Ljava/lang/String; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotificationParams; + 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/types/ElicitationCompleteNotificationParams;)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/types/ElicitationCompleteNotificationParams$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitationKt { public static final fun ElicitRequestParams-EDEVuBg (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; public static synthetic fun ElicitRequestParams-EDEVuBg$default (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestFormParams; @@ -2939,6 +2998,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Defined : jav public static final field Initialize Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field LoggingSetLevel Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field NotificationsCancelled Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; + public static final field NotificationsElicitationComplete Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field NotificationsInitialized Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field NotificationsMessage Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field NotificationsProgress Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt index 1d96fea03..5697df831 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt @@ -32,6 +32,7 @@ public sealed interface Method { NotificationsToolsListChanged("notifications/tools/list_changed"), NotificationsRootsListChanged("notifications/roots/list_changed"), NotificationsPromptsListChanged("notifications/prompts/list_changed"), + NotificationsElicitationComplete("notifications/elicitation/complete"), NotificationsTasksStatus("notifications/tasks/status"), ToolsList("tools/list"), ToolsCall("tools/call"), diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt index cb1aa9908..08b1f12ab 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt @@ -330,6 +330,36 @@ public data class RootsListChangedNotification(override val params: BaseNotifica override val method: Method = Method.Defined.NotificationsRootsListChanged } +// ============================================================================ +// Elicitation Complete Notification +// ============================================================================ + +/** + * An optional notification from the server to the client, + * informing it of a completion of an out-of-band elicitation request. + * + * @property params Parameters identifying which elicitation completed. + */ +@Serializable +public data class ElicitationCompleteNotification(override val params: ElicitationCompleteNotificationParams) : + ServerNotification { + @EncodeDefault + override val method: Method = Method.Defined.NotificationsElicitationComplete +} + +/** + * Parameters for a notifications/elicitation/complete notification. + * + * @property elicitationId The ID of the elicitation that completed. + * @property meta Optional metadata for this notification. + */ +@Serializable +public data class ElicitationCompleteNotificationParams( + val elicitationId: String, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : NotificationParams + // ============================================================================ // Tools List Changed Notification // ============================================================================ diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt index 7f1c81dff..3e259575b 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt @@ -241,6 +241,7 @@ private val serverNotificationDeserializers: Map(McpJson, json) + val params = notification.params + + assertEquals(Method.Defined.NotificationsElicitationComplete, notification.method) + assertEquals("elicit-99", params.elicitationId) + assertNull(params.meta) + } + @Test fun `should serialize TaskStatusNotification with all fields`() { val notification = TaskStatusNotification( diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index 70e8e1401..80da5b53e 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -14,6 +14,7 @@ public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/Client public static synthetic fun notification$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ServerNotification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public abstract fun ping (Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun ping$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun sendElicitationComplete (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun sendPromptListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun sendResourceListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -139,6 +140,7 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server { 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 sendElicitationComplete (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun sendLoggingMessage (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun sendPromptListChanged (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun sendResourceListChanged (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -182,6 +184,7 @@ public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelc 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 sendElicitationComplete (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/types/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; diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt index 96177eede..23dbe89a6 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt @@ -9,6 +9,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult @@ -165,6 +166,13 @@ public interface ClientConnection { * Sends a notification to the client indicating that the list of prompts has changed. */ public suspend fun sendPromptListChanged() + + /** + * Sends a notification to the client indicating that an out-of-band elicitation has completed. + * + * @param notification Details of the completed elicitation. + */ + public suspend fun sendElicitationComplete(notification: ElicitationCompleteNotification) } @Suppress("TooManyFunctions") @@ -270,6 +278,11 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client notification(PromptListChangedNotification()) } + override suspend fun sendElicitationComplete(notification: ElicitationCompleteNotification) { + logger.debug { "Sending elicitation complete notification for: ${notification.params.elicitationId}" } + notification(notification) + } + /** * Determines whether a message with the specified logging level is accepted * based on the current logging level of the session. diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index a3b225667..de219d4e6 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -10,6 +10,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest @@ -850,6 +851,16 @@ public open class Server( public suspend fun sendPromptListChanged(sessionId: String) { clientConnection(sessionId).sendPromptListChanged() } + + /** + * Triggers [ClientConnection.sendElicitationComplete] for session by provided [sessionId]. + * + * @param sessionId The session ID to send the elicitation complete notification to. + * @param notification Details of the completed elicitation. + */ + public suspend fun sendElicitationComplete(sessionId: String, notification: ElicitationCompleteNotification) { + clientConnection(sessionId).sendElicitationComplete(notification) + } // End the ServerSession / ClientConnection redirection section // Start the notification handling section diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt index 9e6553a0e..e548e0315 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -9,6 +9,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation @@ -432,5 +433,13 @@ public open class ServerSession( * Sends a notification to the client indicating that the list of prompts has changed. */ public suspend fun sendPromptListChanged(): Unit = clientConnection.sendPromptListChanged() + + /** + * Sends a notification to the client indicating that an out-of-band elicitation has completed. + * + * @param notification Details of the completed elicitation. + */ + public suspend fun sendElicitationComplete(notification: ElicitationCompleteNotification): Unit = + clientConnection.sendElicitationComplete(notification) // End the ClientConnection redirection section } From 33923bc7c9f74e640f30385c4af0366fae88da8c Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Thu, 2 Apr 2026 14:41:48 +0200 Subject: [PATCH 066/187] feat(docs): public api kdocs (#658) - enable rule for checking undocumented public api - add kdoc skill ## How Has This Been Tested? ./gradlew detekt ## Breaking Changes none ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [x] I have added or updated documentation as needed --------- Co-authored-by: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> --- .claude/skills/kdoc/SKILL.md | 74 +++++++++++++++++++ config/detekt/detekt.yml | 19 +++++ .../client/StreamableHttpClientTransport.kt | 7 +- .../kotlin/sdk/internal/utils.kt | 1 + .../kotlin/sdk/shared/Protocol.kt | 13 ++++ .../kotlin/sdk/shared/TransportSendOptions.kt | 6 ++ .../sdk/shared/WebSocketMcpTransport.kt | 1 + .../kotlin/sdk/types/PingRequest.kt | 5 ++ .../kotlin/sdk/types/capabilities.kt | 8 ++ .../kotlin/sdk/types/common.kt | 13 +++- .../kotlin/sdk/types/completion.kt | 1 + .../kotlin/sdk/types/content.kt | 14 +++- .../kotlin/sdk/types/elicitation.kt | 4 + .../kotlin/sdk/types/jsonRpc.kt | 18 ++++- .../kotlin/sdk/types/jsonUtils.kt | 2 + .../kotlin/sdk/types/methods.kt | 2 + .../kotlin/sdk/types/notification.kt | 4 + .../kotlin/sdk/types/request.kt | 25 ++++++- .../kotlin/sdk/types/resources.kt | 3 + .../kotlin/sdk/types/roots.kt | 1 + .../kotlin/sdk/types/sampling.kt | 11 ++- .../kotlin/sdk/types/schema.kt | 10 +++ .../kotlin/sdk/types/tasks.kt | 9 +++ .../sdk/utils/ResourceTemplateMatcher.kt | 2 + .../kotlin/sdk/internal/utils.jvm.kt | 1 + .../kotlin/sdk/server/SSEServerTransport.kt | 2 + .../kotlin/sdk/server/ServerSession.kt | 2 + .../server/StreamableHttpServerTransport.kt | 29 +++----- 28 files changed, 260 insertions(+), 27 deletions(-) create mode 100644 .claude/skills/kdoc/SKILL.md diff --git a/.claude/skills/kdoc/SKILL.md b/.claude/skills/kdoc/SKILL.md new file mode 100644 index 000000000..476029367 --- /dev/null +++ b/.claude/skills/kdoc/SKILL.md @@ -0,0 +1,74 @@ +--- +name: kdoc +description: "Add KDoc documentation to Kotlin public API. Use whenever the user asks to document Kotlin code, add KDoc, generate API docs, mentions undocumented public declarations, or wants to improve existing documentation. Also trigger when the user says 'add docs', 'document this class/file/module', 'write KDoc', or asks about missing documentation in Kotlin code." +--- + +# KDoc Generator + +Add KDoc comments to public Kotlin API declarations. + +## What to document + +All public declarations (no explicit `private`/`internal`/`protected`): +- Classes, interfaces, objects, sealed classes/interfaces +- Functions, extension functions +- Properties +- Enum classes and entries +- Type aliases, annotation classes + +Include `@Deprecated` elements. + +## Context + +Read the implementation, not just the signature, to write accurate descriptions. Understanding what the code actually does prevents superficial or misleading documentation. + +## KDoc format + +**Class/interface example:** + +````kotlin +/** + * Manages active client sessions and their lifecycle. + * + * Sessions are created on first connection and cleaned up + * when the transport closes. + * + * @property maxSessions upper limit on concurrent sessions + * @property timeout idle timeout before a session is evicted + */ +```` + +**Function example:** + +````kotlin +/** + * Registers a new tool with the given handler. + * + * Example: + * ```kotlin + * server.addTool( + * name = "echo", + * description = "Echoes input back", + * ) { request -> + * CallToolResult(content = listOf(TextContent("Echo: ${request.arguments}"))) + * } + * ``` + * + * @param name unique tool identifier + * @param description human-readable tool description + * @param handler suspend function invoked when the tool is called + * @return the registered tool definition + */ +```` + +## Rules + +- Summary: concise first sentence starting with a third-person verb ("Creates", "Returns", "Represents"). Expand to 2-3 sentences only when genuinely complex +- **@property** in class-level KDoc for all public properties (never as individual KDoc on the property); **@param** for function parameters +- **@return** for non-Unit return types +- **Example block**: add for DSL builders, complex functions, extension functions with non-obvious usage. Skip for trivial one-liners and simple getters +- **DSL builders**: always include an Example showing full usage with the receiver scope — this is critical for discoverability +- KDoc links (`[ClassName]`, `[methodName]`): only where it adds clear navigational value +- No **@throws** — don't document exceptions +- No **suspend** notes — coroutine nature is visible from the signature +- **Existing KDoc**: rewrite if incomplete (missing @param/@return/@property) or low quality \ No newline at end of file diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 2cb135abb..4d4473430 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -22,3 +22,22 @@ empty-blocks: EmptyFunctionBlock: excludes: *testFolders +comments: + UndocumentedPublicClass: + active: true + excludes: &testAndGeneratedFolders + - '**/test/**' + - '**/commonTest/**' + - '**/jvmTest/**' + - '**/jsTest/**' + - '**/iosTest/**' + - '**/generated-sources/**' + ignoreDefaultCompanionObject: true + UndocumentedPublicFunction: + active: true + excludes: *testAndGeneratedFolders + UndocumentedPublicProperty: + active: true + excludes: *testAndGeneratedFolders + searchProtectedProperty: false + ignoreEnumEntries: true diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt index 80f37aedc..999e25acb 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt @@ -51,7 +51,9 @@ private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID" /** - * Error class for Streamable HTTP transport errors. + * Represents an error from the Streamable HTTP transport. + * + * @property code HTTP status code associated with the error, or `null` if unavailable */ public class StreamableHttpError(public val code: Int? = null, message: String? = null) : Exception("Streamable HTTP error: $message") @@ -66,6 +68,9 @@ private sealed interface ConnectResult { * Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. * It will connect to a server using HTTP POST for sending messages and HTTP GET with Server-Sent Events * for receiving messages. + * + * @property sessionId session identifier assigned by the server after initialization, or `null` before connection + * @property protocolVersion MCP protocol version negotiated with the server, or `null` before connection */ @Suppress("TooManyFunctions") public class StreamableHttpClientTransport( diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt index d1cbe781f..37a57b76c 100644 --- a/kotlin-sdk-core/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,5 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher +/** Platform-specific [CoroutineDispatcher] for I/O-bound operations. */ public expect val IODispatcher: CoroutineDispatcher diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 20739e0ad..43c0ae583 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -42,6 +42,7 @@ import kotlin.time.Duration.Companion.seconds private val logger = KotlinLogging.logger { } +/** Default implementation name used in MCP handshake. */ public const val IMPLEMENTATION_NAME: String = "mcp-ktor" /** @@ -51,6 +52,8 @@ public typealias ProgressCallback = (Progress) -> Unit /** * Additional initialization options. + * + * @property timeout default timeout for outgoing requests */ public open class ProtocolOptions( /** @@ -97,9 +100,13 @@ public class RequestOptions( public val onProgress: ProgressCallback? = null, public val timeout: Duration = DEFAULT_REQUEST_TIMEOUT, ) : TransportSendOptions(relatedRequestId, resumptionToken, onResumptionToken) { + /** Destructuring component for [onProgress]. */ public operator fun component4(): ProgressCallback? = onProgress + + /** Destructuring component for [timeout]. */ public operator fun component5(): Duration = timeout + /** Creates a copy of this [RequestOptions] with the specified fields replaced. */ public fun copy( relatedRequestId: RequestId? = this.relatedRequestId, resumptionToken: String? = this.resumptionToken, @@ -139,6 +146,12 @@ internal val COMPLETED = CompletableDeferred(Unit).also { it.complete(Unit) } /** * Implements MCP protocol framing on top of a pluggable transport, including * features like request/response linking, notifications, and progress. + * + * @property transport the active transport, or `null` if not connected + * @property requestHandlers registered request handlers keyed by method name + * @property notificationHandlers registered notification handlers keyed by method name + * @property responseHandlers pending response handlers keyed by request ID + * @property progressHandlers registered progress callbacks keyed by progress token */ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptions?) { public var transport: Transport? = null diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt index c56df04e9..549028881 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt @@ -17,10 +17,16 @@ public open class TransportSendOptions( public val resumptionToken: String? = null, public val onResumptionToken: ((String) -> Unit)? = null, ) { + /** Destructuring component for [relatedRequestId]. */ public operator fun component1(): RequestId? = relatedRequestId + + /** Destructuring component for [resumptionToken]. */ public operator fun component2(): String? = resumptionToken + + /** Destructuring component for [onResumptionToken]. */ public operator fun component3(): ((String) -> Unit)? = onResumptionToken + /** Creates a copy of this [TransportSendOptions] with the specified fields replaced. */ public open fun copy( relatedRequestId: RequestId? = this.relatedRequestId, resumptionToken: String? = this.resumptionToken, diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt index de75cc460..5f8bf9dac 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.launch import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi +/** WebSocket subprotocol identifier for MCP connections. */ public const val MCP_SUBPROTOCOL: String = "mcp" private val logger = KotlinLogging.logger {} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt index 2f1e3ae69..c45ed301b 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt @@ -4,6 +4,11 @@ import kotlinx.serialization.EncodeDefault import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable +/** + * Represents a ping request used to check if the connection is alive. + * + * @property meta optional request metadata + */ @Serializable public data class PingRequest(override val params: BaseRequestParams? = null) : ClientRequest, diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt index cff3b8906..fae24c319 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt @@ -54,6 +54,10 @@ public data class ClientCapabilities( public val experimental: JsonObject? = null, ) { + /** + * @property sampling convenience value to enable the sampling capability + * @property elicitation convenience value to enable the elicitation capability + */ public companion object { public val sampling: JsonObject = EmptyJsonObject public val elicitation: JsonObject = EmptyJsonObject @@ -100,6 +104,10 @@ public data class ServerCapabilities( val experimental: JsonObject? = null, ) { + /** + * @property Logging convenience value to enable the logging capability + * @property Completions convenience value to enable the completions capability + */ public companion object { public val Logging: JsonObject = EmptyJsonObject public val Completions: JsonObject = EmptyJsonObject diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt index 3ae89979d..ac867af1d 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt @@ -8,10 +8,13 @@ import kotlinx.serialization.json.JsonObject // Protocol Version Constants // ============================================================================ +/** The latest supported MCP protocol version string. */ public const val LATEST_PROTOCOL_VERSION: String = "2025-11-25" +/** The default protocol version used when negotiation is not performed. */ public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-06-18" +/** All MCP protocol versions supported by this SDK. */ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( LATEST_PROTOCOL_VERSION, "2025-06-18", @@ -25,6 +28,8 @@ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( /** * Represents an entity that includes additional metadata in its responses. + * + * @property meta optional metadata attached to this entity */ @Serializable public sealed interface WithMeta { @@ -123,13 +128,19 @@ public enum class Role { * * References are used to point to other entities (prompts, resources, etc.) * without including their full definitions. + * + * @property type discriminator identifying the reference subtype */ @Serializable(with = ReferencePolymorphicSerializer::class) public sealed interface Reference { public val type: ReferenceType } -/** Discriminator for [Reference] subtypes used in completion and other operations. */ +/** + * Discriminator for [Reference] subtypes used in completion and other operations. + * + * @property value serialized string representation of this reference type + */ @Serializable public enum class ReferenceType(public val value: String) { @SerialName("ref/prompt") diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt index 79a76ea53..c782e3e7d 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt @@ -12,6 +12,7 @@ import kotlinx.serialization.json.JsonObject * A request from the client to the server to ask for completion options. * * @property params The request parameters containing the argument to complete and its context. + * @property meta optional request metadata */ @Serializable public data class CompleteRequest(override val params: CompleteRequestParams) : ClientRequest { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt index 45cfd0ddb..a1c9e4fec 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt @@ -8,7 +8,11 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject -/** Discriminator values for the polymorphic [ContentBlock] hierarchy. */ +/** + * Discriminator values for the polymorphic [ContentBlock] hierarchy. + * + * @property value serialized string representation of this content type + */ @Serializable public enum class ContentTypes(public val value: String) { @SerialName("text") @@ -27,11 +31,19 @@ public enum class ContentTypes(public val value: String) { EMBEDDED_RESOURCE("resource"), } +/** + * Base interface for all content blocks in the protocol. + * + * @property type discriminator identifying the content block subtype + */ @Serializable(with = ContentBlockPolymorphicSerializer::class) public sealed interface ContentBlock : WithMeta { public val type: ContentTypes } +/** + * Content block that carries media data such as text, images, or audio. + */ @Serializable(with = MediaContentPolymorphicSerializer::class) public sealed interface MediaContent : ContentBlock diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt index fc7787b49..bb303f0c6 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt @@ -46,6 +46,8 @@ public data class ElicitRequest(override val params: ElicitRequestParams) : Serv * Represents the parameters for an `elicitation/create` request. * * Implementations: [ElicitRequestFormParams], [ElicitRequestURLParams]. + * + * @property message The message to present to the user describing what information is being requested. */ @Serializable(with = ElicitRequestParamsSerializer::class) public sealed interface ElicitRequestParams : RequestParams { @@ -111,6 +113,7 @@ public fun ElicitRequestParams( * later via `tasks/result`. * @property requestedSchema A restricted subset of JSON Schema. Only top-level properties * are allowed, without nesting. + * @property mode The elicitation mode discriminator, always `"form"`. * @property meta Optional metadata. May include a progressToken for out-of-band progress notifications. */ @Serializable @@ -138,6 +141,7 @@ public data class ElicitRequestFormParams( * @property task If specified, the caller is requesting task-augmented execution. The request * will return a [CreateTaskResult] immediately, and the actual result can be retrieved * later via `tasks/result`. + * @property mode The elicitation mode discriminator, always `"url"`. * @property meta Optional metadata. May include a progressToken for out-of-band progress notifications. */ @Serializable diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt index f42139f55..1a9ea2eca 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt @@ -14,6 +14,7 @@ import kotlin.jvm.JvmOverloads import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +/** JSON-RPC protocol version used by MCP (`"2.0"`). */ public const val JSONRPC_VERSION: String = "2.0" /** @@ -40,12 +41,20 @@ public fun RequestId(value: Long): RequestId = RequestId.NumberId(value) @Serializable(with = RequestIdPolymorphicSerializer::class) public sealed interface RequestId { - /** A string-based request identifier. */ + /** + * A string-based request identifier. + * + * @property value the string representation of this request ID + */ @JvmInline @Serializable public value class StringId(public val value: String) : RequestId - /** A numeric request identifier. */ + /** + * A numeric request identifier. + * + * @property value the numeric representation of this request ID + */ @JvmInline @Serializable public value class NumberId(public val value: Long) : RequestId @@ -95,12 +104,17 @@ internal fun JSONRPCNotification.fromJSON(): Notification = * Base interface for all JSON-RPC 2.0 messages. * * All messages in the MCP protocol follow the JSON-RPC 2.0 specification. + * + * @property jsonrpc the JSON-RPC protocol version, always `"2.0"` */ @Serializable(with = JSONRPCMessagePolymorphicSerializer::class) public sealed interface JSONRPCMessage { public val jsonrpc: String } +/** + * Represents an empty JSON-RPC message used as a placeholder or no-op response. + */ @Serializable public data object JSONRPCEmptyMessage : JSONRPCMessage { override val jsonrpc: String = JSONRPC_VERSION diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonUtils.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonUtils.kt index 566905fb5..d83e01231 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonUtils.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonUtils.kt @@ -11,8 +11,10 @@ import kotlinx.serialization.json.add import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject +/** Reusable empty [JsonObject] instance. */ public val EmptyJsonObject: JsonObject = JsonObject(emptyMap()) +/** Pre-configured [Json] instance for MCP serialization. */ @OptIn(ExperimentalSerializationApi::class) public val McpJson: Json by lazy { Json { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt index 5697df831..dae2fc742 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt @@ -4,6 +4,8 @@ import kotlinx.serialization.Serializable /** * Represents a method in the protocol, which can be predefined or custom. + * + * @property value the string representation of this method name */ @Serializable(with = MethodSerializer::class) public sealed interface Method { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt index 08b1f12ab..24be040c9 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt @@ -11,6 +11,9 @@ import kotlinx.serialization.json.JsonObject /** * Represents a notification in the protocol. + * + * @property method the notification method identifier + * @property params optional notification parameters */ @Serializable(with = NotificationPolymorphicSerializer::class) public sealed interface Notification { @@ -73,6 +76,7 @@ public class Progress( * @property method The custom method name. By convention, custom methods often contain * organization-specific prefixes (e.g., "mycompany/custom_event"). * @property params Raw JSON parameters for the custom notification, if present. + * @property meta optional metadata for this notification */ @Serializable public data class CustomNotification(override val method: Method, override val params: BaseNotificationParams? = null) : diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt index 683186c15..9f52ec9e8 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt @@ -11,6 +11,12 @@ import kotlinx.serialization.json.long import kotlinx.serialization.json.longOrNull import kotlin.jvm.JvmInline +/** + * Metadata attached to a request's `_meta` field. + * + * @property json the raw JSON object containing the metadata + * @property progressToken optional progress token for tracking request progress + */ @JvmInline @Serializable public value class RequestMeta(public val json: JsonObject) { @@ -43,6 +49,9 @@ public value class RequestMeta(public val json: JsonObject) { public operator fun get(key: String): JsonElement? = json[key] } +/** + * Base interface for parameters attached to a [Request]. + */ @Serializable public sealed interface RequestParams { /** @@ -55,11 +64,17 @@ public sealed interface RequestParams { public val meta: RequestMeta? } +/** + * Default [RequestParams] implementation carrying only optional metadata. + */ @Serializable public data class BaseRequestParams(@SerialName("_meta") override val meta: RequestMeta? = null) : RequestParams /** * Represents a request in the protocol. + * + * @property method the request method identifier + * @property params optional request parameters */ @OptIn(ExperimentalSerializationApi::class) @Serializable(with = RequestPolymorphicSerializer::class) @@ -112,9 +127,15 @@ public sealed interface PaginatedRequest : Request { @Serializable(with = RequestResultPolymorphicSerializer::class) public sealed interface RequestResult : WithMeta +/** + * Represents a result returned by the server in response to a [ClientRequest]. + */ @Serializable(with = ClientResultPolymorphicSerializer::class) public sealed interface ClientResult : RequestResult +/** + * Represents a result returned by the client in response to a [ServerRequest]. + */ @Serializable(with = ServerResultPolymorphicSerializer::class) public sealed interface ServerResult : RequestResult @@ -129,7 +150,9 @@ public data class EmptyResult(@SerialName("_meta") override val meta: JsonObject ServerResult /** - * Represents a request supporting pagination. + * Represents a paginated result. + * + * @property nextCursor opaque token for retrieving the next page of results, or `null` if no more results */ @Serializable public sealed interface PaginatedResult : RequestResult { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt index fa316dec5..86ce3cee7 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt @@ -8,6 +8,9 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject +/** + * Common interface for resource-like types that share URI, name, and description fields. + */ @Serializable public sealed interface ResourceLike : WithMeta diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt index af7b1e7a2..194ea4fb2 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt @@ -53,6 +53,7 @@ public data class Root( * `listChanged = true` to receive these requests. * * @property params Optional request parameters containing metadata. + * @property meta optional request metadata */ @Serializable public data class ListRootsRequest(override val params: BaseRequestParams? = null) : ServerRequest { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt index 1ade1bfc5..97fc0c06c 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt @@ -243,10 +243,19 @@ public data class CreateMessageResult( override val meta: JsonObject? = null, ) : ClientResult -/** The reason why the LLM stopped generating tokens. */ +/** + * The reason why the LLM stopped generating tokens. + * + * @property value the string representation of this stop reason + */ @JvmInline @Serializable public value class StopReason(public val value: String) { + /** + * @property EndTurn generation ended naturally + * @property StopSequence a stop sequence was encountered + * @property MaxTokens the maximum token limit was reached + */ public companion object { public val EndTurn: StopReason = StopReason("endTurn") public val StopSequence: StopReason = StopReason("stopSequence") diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt index dda2ac6d4..479cc66f0 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt @@ -24,6 +24,7 @@ public sealed interface PrimitiveSchemaDefinition * @property minLength Minimum string length. * @property maxLength Maximum string length. * @property format Optional format constraint (e.g., email, URI, date). + * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Serializable @@ -72,6 +73,7 @@ public sealed interface NumberSchemaDefinition : PrimitiveSchemaDefinition * @property description Optional description for the field. * @property minimum Minimum allowed value. * @property maximum Maximum allowed value. + * @property type JSON Schema type discriminator, always `"integer"`. * @property default Optional default value. */ @Serializable @@ -93,6 +95,7 @@ public data class IntegerSchema( * @property description Optional description for the field. * @property minimum Minimum allowed value. * @property maximum Maximum allowed value. + * @property type JSON Schema type discriminator, always `"number"`. * @property default Optional default value. */ @Serializable @@ -112,6 +115,7 @@ public data class DoubleSchema( * * @property title Optional display title for the field. * @property description Optional description for the field. + * @property type JSON Schema type discriminator, always `"boolean"`. * @property default Optional default value. */ @Serializable @@ -157,6 +161,7 @@ public sealed interface SingleSelectEnumSchema : EnumSchemaDefinition * @property title Optional display title for the field. * @property description Optional description for the field. * @property enumValues Array of enum values to choose from. + * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Serializable @@ -177,6 +182,7 @@ public data class UntitledSingleSelectEnumSchema( * @property title Optional display title for the field. * @property description Optional description for the field. * @property oneOf Array of enum options with values and display labels. + * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Serializable @@ -199,6 +205,7 @@ public data class TitledSingleSelectEnumSchema( * @property description Optional description for the field. * @property enumValues Array of enum values to choose from. * @property enumNames Display names for enum values. Non-standard according to JSON Schema 2020-12. + * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Deprecated("Use TitledSingleSelectEnumSchema instead") @@ -231,6 +238,7 @@ public sealed interface MultiSelectEnumSchema : EnumSchemaDefinition * @property minItems Minimum number of items to select. * @property maxItems Maximum number of items to select. * @property items Schema for the array items. + * @property type JSON Schema type discriminator, always `"array"`. * @property default Optional default value. */ @Serializable @@ -248,6 +256,7 @@ public data class UntitledMultiSelectEnumSchema( /** * Schema for the array items with plain enum values. * + * @property type JSON Schema type discriminator, always `"string"`. * @property enumValues Array of enum values to choose from. */ @Serializable @@ -268,6 +277,7 @@ public data class UntitledMultiSelectEnumSchema( * @property minItems Minimum number of items to select. * @property maxItems Maximum number of items to select. * @property items Schema for array items with enum options and display labels. + * @property type JSON Schema type discriminator, always `"array"`. * @property default Optional default value. */ @Serializable diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt index 42b5752af..0f154e5c0 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt @@ -17,6 +17,14 @@ public const val RELATED_TASK_META_KEY: String = "io.modelcontextprotocol/relate /** * Common fields shared by all types representing the task state. + * + * @property taskId the task identifier + * @property status current task state + * @property statusMessage optional human-readable message describing the current task state + * @property createdAt ISO 8601 timestamp when the task was created + * @property lastUpdatedAt ISO 8601 timestamp when the task was last updated + * @property ttl actual retention duration from creation in milliseconds, or `null` for unlimited + * @property pollInterval suggested polling interval in milliseconds */ public sealed interface TaskFields { public val taskId: String @@ -228,6 +236,7 @@ public data class GetTaskPayloadRequestParams( * The structure matches the result type of the original request. * For example, a tools/call task would return the [CallToolResult] structure. * + * @property json the raw JSON object containing the result payload * @property meta Optional metadata for this response. */ @JvmInline diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt index 5ba956d29..867cc9f1e 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt @@ -32,6 +32,8 @@ public class MatchResult(public val variables: Map, public val s * candidate URIs against that template via [match]. The returned [MatchResult.score] * must reflect match specificity so that a selection algorithm can prefer the most * specific template when multiple templates match the same URI. + * + * @property resourceTemplate the template to match URIs against */ public interface ResourceTemplateMatcher { diff --git a/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt b/kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt index d00c1ab55..35a1e9802 100644 --- a/kotlin-sdk-core/src/jvmMain/kotlin/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,6 @@ package io.modelcontextprotocol.kotlin.sdk.internal import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +/** Platform-specific [CoroutineDispatcher] for I/O-bound operations. */ public actual val IODispatcher: CoroutineDispatcher get() = Dispatchers.IO diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt index 619266aa7..f8c658129 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt @@ -26,6 +26,8 @@ internal const val SESSION_ID_PARAM = "sessionId" * Server transport for SSE: this will send messages over an SSE connection and receive messages from HTTP POST requests. * * Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`. + * + * @property sessionId unique identifier for this transport session, generated randomly on creation */ @OptIn(ExperimentalAtomicApi::class) public class SseServerTransport(private val endpoint: String, private val session: ServerSSESession) : diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt index e548e0315..2f55c7642 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -40,6 +40,8 @@ private val logger = KotlinLogging.logger {} /** * Represents a server session. + * + * @property sessionId unique identifier for this session, generated randomly on creation */ @Suppress("TooManyFunctions") public open class ServerSession( diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 1ea8d83d0..70948d9ce 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -71,6 +71,7 @@ private data class SessionContext(val session: ServerSSESession?, val call: Appl * - No session validation is performed * * @param configuration Transport configuration. See [Configuration] for available options. + * @property sessionId session identifier assigned after initialization, or `null` in stateless mode */ @OptIn(ExperimentalUuidApi::class, ExperimentalAtomicApi::class) @Suppress("TooManyFunctions") @@ -122,28 +123,16 @@ public class StreamableHttpServerTransport(private val configuration: Configurat ) /** - * Configuration for managing various aspects of the StreamableHttpServerTransport. - * - * @property enableJsonResponse Determines whether the server should return JSON responses. - * Defaults to `false`. - * - * @property enableDnsRebindingProtection Enables DNS rebinding protection. - * Defaults to `false`. - * - * @property allowedHosts A list of hosts allowed for server communication. - * Defaults to `null`, allowing all hosts. - * - * @property allowedOrigins A list of allowed origins for CORS (Cross-Origin Resource Sharing). - * Defaults to `null`, allowing all origins. - * - * @property eventStore The `EventStore` instance for handling resumable events. - * Defaults to `null`, disabling resumability. - * - * @property retryInterval Retry interval for event handling or reconnection attempts. - * Defaults to `null`. + * Configuration options for [StreamableHttpServerTransport]. * + * @property enableJsonResponse when `true`, returns direct JSON responses instead of SSE streams + * @property enableDnsRebindingProtection enables DNS rebinding protection + * @property allowedHosts list of hosts allowed for server communication, or `null` to allow all + * @property allowedOrigins list of allowed CORS origins, or `null` to allow all + * @property eventStore store for resumable events, or `null` to disable resumability + * @property retryInterval retry interval for SSE reconnection attempts * @property maxRequestBodySize Maximum allowed size (in bytes) for incoming request bodies. - * Defaults to 4 MB (4,194,304 bytes). + * Defaults to 4 MB (4,194,304 bytes). */ public class Configuration( public val enableJsonResponse: Boolean = false, From c470c5df535b9f679f11c380998cf07ca733efe6 Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:07:38 +0300 Subject: [PATCH 067/187] chore: bump project version to 0.11.0 (#669) bump project version to 0.11.0 ## Motivation and Context Release 0.11 ## How Has This Been Tested? Regression ## Breaking Changes No ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 1d5620a7e..09a658be2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,4 +8,4 @@ kotlin.daemon.jvmargs=-Xmx4G kotlin.native.ignoreDisabledTargets=true group=io.modelcontextprotocol -version=0.10.0 +version=0.11.0 From 0ab31cfa4be8858d95f6266010df576319c650f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 05:13:07 +0000 Subject: [PATCH 068/187] chore(deps): bump io.ktor:ktor-bom from 3.2.3 to 3.3.3 in /samples/weather-stdio-server (#656) --- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index 0b75f097f..c1a799716 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] coroutines = "1.10.2" kotlin = "2.2.21" -ktor = "3.2.3" +ktor = "3.3.3" logging = "8.0.01" mcp-kotlin = "0.10.0" shadow = "9.4.1" From f8f04d623ccdfa949fbbd0c80bd53ecfc53e2942 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 05:28:39 +0000 Subject: [PATCH 069/187] chore(deps): bump io.ktor:ktor-bom from 3.2.3 to 3.3.3 in /samples/simple-streamable-server (#653) --- samples/simple-streamable-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index a9fced704..77707e09f 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin = "2.2.21" -ktor = "3.2.3" +ktor = "3.3.3" junit = "6.0.3" mcp-kotlin = "0.10.0" slf4j = "2.0.17" From 7cf65c6e6e2a877f2cbf2c323cf6959ac934882a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:56:55 +0300 Subject: [PATCH 070/187] chore(deps): bump the all-dependencies group in /integration-test/src/jvmTest/typescript with 2 updates (#677) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-dependencies group in /integration-test/src/jvmTest/typescript with 2 updates: [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@modelcontextprotocol/sdk` from 1.28.0 to 1.29.0
Release notes

Sourced from @​modelcontextprotocol/sdk's releases.

v1.29.0

What's Changed

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/typescript-sdk/compare/v1.28.0...v1.29.0

Commits

Updates `@types/node` from 25.5.0 to 25.5.2
Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/jvmTest/typescript/package-lock.json | 16 ++++++++-------- .../src/jvmTest/typescript/package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index ca30f6578..7d96bae81 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -8,14 +8,14 @@ "name": "mcp-typescript-test-sdk", "version": "1.0.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.28.0", + "@modelcontextprotocol/sdk": "^1.29.0", "express": "^5.2.1", "zod": "^4.3.6" }, "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.5.0", + "@types/node": "^25.5.2", "cors": "^2.8.6", "tsx": "^4.21.0", "typescript": "^6.0.2" @@ -476,9 +476,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz", - "integrity": "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -579,9 +579,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index 0a43cd229..20249d035 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -9,14 +9,14 @@ "server:http": "tsx server/http-server.ts" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.28.0", + "@modelcontextprotocol/sdk": "^1.29.0", "express": "^5.2.1", "zod": "^4.3.6" }, "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.5.0", + "@types/node": "^25.5.2", "cors": "^2.8.6", "tsx": "^4.21.0", "typescript": "^6.0.2" From f0f8b30729fed2a19408d44032a52f5931f9cb2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:30:09 +0300 Subject: [PATCH 071/187] chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/weather-stdio-server (#676) --- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index c1a799716..cc2b53a2c 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ coroutines = "1.10.2" kotlin = "2.2.21" ktor = "3.3.3" logging = "8.0.01" -mcp-kotlin = "0.10.0" +mcp-kotlin = "0.11.0" shadow = "9.4.1" slf4j = "2.0.17" From 7c87ed1ba5de505b0af11df6cb5b349515826974 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 07:53:14 +0000 Subject: [PATCH 072/187] chore(deps): bump the kotest group with 3 updates (#674) --- 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 78f55fe46..f2836ef7f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ openapi-generator = "7.21.0" awaitility = "4.3.0" collections-immutable = "0.4.0" coroutines = "1.10.2" -kotest = "6.1.9" +kotest = "6.1.11" kotlinx-io = "0.9.0" ktor = "3.3.3" logging = "8.0.01" From 3f7f499dbe5dddf456b01b35a08bacfe23b1b548 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 08:18:58 +0000 Subject: [PATCH 073/187] chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/kotlin-mcp-server (#673) --- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index 1c59a88b6..05007d0f7 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.2.21" ktor = "3.3.3" -mcp-kotlin = "0.10.0" +mcp-kotlin = "0.11.0" slf4j = "2.0.17" shadow = "9.4.1" From 4144db67365f8c8b9c20647c42facfcf4513dee8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 08:33:49 +0000 Subject: [PATCH 074/187] chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.10.0 to 0.11.0 in /samples/kotlin-mcp-client (#672) --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 38cb588e3..8c9344818 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -2,7 +2,7 @@ anthropic = "2.18.0" kotlin = "2.2.21" ktor = "3.2.3" -mcp-kotlin = "0.10.0" +mcp-kotlin = "0.11.0" shadow = "9.4.1" slf4j = "2.0.17" From 20ac69f9af424e6f29d74a5f01dce7c4e5fd0c88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:37:28 +0000 Subject: [PATCH 075/187] chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/simple-streamable-server (#675) --- samples/simple-streamable-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index 77707e09f..e74e9df75 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin = "2.2.21" ktor = "3.3.3" junit = "6.0.3" -mcp-kotlin = "0.10.0" +mcp-kotlin = "0.11.0" slf4j = "2.0.17" shadow = "9.4.1" From 8442ab852b39a4c5493397976edd6232a10449ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:07:04 +0000 Subject: [PATCH 076/187] chore(deps): bump com.anthropic:anthropic-java from 2.18.0 to 2.19.0 in /samples/kotlin-mcp-client in the other-dependencies group across 1 directory (#671) --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 8c9344818..130a75b2d 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -anthropic = "2.18.0" +anthropic = "2.19.0" kotlin = "2.2.21" ktor = "3.2.3" mcp-kotlin = "0.11.0" From 66d17956c1390b5eda195c2d3a54054ce84e1c76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:29:00 +0200 Subject: [PATCH 077/187] chore(deps): bump @hono/node-server from 1.19.11 to 1.19.13 in /integration-test/src/jvmTest/typescript (#679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@hono/node-server](https://github.com/honojs/node-server) from 1.19.11 to 1.19.13.
Release notes

Sourced from @​hono/node-server's releases.

v1.19.13

Security Fix

Fixed an issue in Serve Static Middleware where inconsistent handling of repeated slashes (//) between the router and static file resolution could allow middleware to be bypassed. Users of Serve Static Middleware are encouraged to upgrade to this version.

See GHSA-92pp-h63x-v22m for details.

v1.19.12

What's Changed

Full Changelog: https://github.com/honojs/node-server/compare/v1.19.11...v1.19.12

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@hono/node-server&package-manager=npm_and_yarn&previous-version=1.19.11&new-version=1.19.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/modelcontextprotocol/kotlin-sdk/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 7d96bae81..24d522730 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -464,9 +464,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", - "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "version": "1.19.13", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", + "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", "license": "MIT", "engines": { "node": ">=18.14.1" From 12171dc2ec9157611d1a2a07ecd6f12a949e5828 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:45:43 +0000 Subject: [PATCH 078/187] chore(deps): bump hono from 4.12.9 to 4.12.12 in /integration-test/src/jvmTest/typescript (#680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [hono](https://github.com/honojs/hono) from 4.12.9 to 4.12.12.
Release notes

Sourced from hono's releases.

v4.12.12

Security fixes

This release includes fixes for the following security issues:

Middleware bypass via repeated slashes in serveStatic

Affects: Serve Static middleware. Fixes a path normalization inconsistency where repeated slashes (//) could bypass route-based middleware protections and allow access to protected static files. GHSA-wmmm-f939-6g9c

Path traversal in toSSG() allows writing files outside the output directory

Affects: toSSG() for Static Site Generation. Fixes a path traversal issue where crafted ssgParams values could write files outside the configured output directory. GHSA-xf4j-xp2r-rqqx

Incorrect IP matching in ipRestriction() for IPv4-mapped IPv6 addresses

Affects: IP Restriction Middleware. Fixes improper handling of IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) that could cause allow/deny rules to be bypassed. GHSA-xpcf-pg52-r92g

Missing validation of cookie name on write path in setCookie()

Affects: setCookie(), serialize(), and serializeSigned() from hono/cookie. Fixes missing validation of cookie names on the write path, preventing inconsistent handling between parsing and serialization. GHSA-26pp-8wgv-hjvm

Non-breaking space prefix bypass in cookie name handling in getCookie()

Affects: getCookie() from hono/cookie. Fixes a discrepancy in cookie name handling that could allow attacker-controlled cookies to override legitimate ones and bypass prefix protections. GHSA-r5rp-j6wh-rvv4


Users who use Serve Static, Static Site Generation, Cookie utilities, or IP restriction middleware are strongly encouraged to upgrade to this version.

v4.12.11

What's Changed

New Contributors

Full Changelog: https://github.com/honojs/hono/compare/v4.12.10...v4.12.11

v4.12.10

What's Changed

New Contributors

... (truncated)

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 24d522730..dd0c9b4a3 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -1197,9 +1197,9 @@ } }, "node_modules/hono": { - "version": "4.12.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", - "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", "license": "MIT", "engines": { "node": ">=16.9.0" From cceca4df58f128b75d9766133b7ecbf986397ccf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:13:53 +0000 Subject: [PATCH 079/187] chore(deps): bump io.ktor:ktor-bom from 3.2.3 to 3.3.3 in /samples/kotlin-mcp-client (#650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [io.ktor:ktor-bom](https://github.com/ktorio/ktor) from 3.2.3 to 3.3.3.
Release notes

Sourced from io.ktor:ktor-bom's releases.

3.3.3

Published 26 November 2025

Improvements

  • KTOR-6837 Discrepancies when parsing URL host with CIO and Darwin engines compared to the rest engines
  • KTOR-9050 Logging: SimpleLogger should be an object, not a class
  • KTOR-9094 Jetty Client: Support HTTP/2 over cleartext (h2c)
  • KTOR-9120 OpenAPI gen: missing operationId for KDoc fields
  • KTOR-3019 Improve logging for CORS plugin

Bugfixes

  • KTOR-8671 Netty: RejectedExecutionException during shutdown on MacOS when dev mode is enabled
  • KTOR-9096 Darwin: New SSE handlers stop responding after canceling few SSE sessions
  • KTOR-9125 Double ResponseSent invocation when exception is thrown after respond
  • KTOR-8878 OpenAPI: StackOverflowError when a response object has property with @Contextual serializer
  • KTOR-8947 Java, ContentEncoding: IllegalHeaderNameException is thrown for ":status" pseudo header with HTTP/2
  • KTOR-9092 NettyHttp2Handler throws IllegalArgumentException: 'ktor.ApplicationCall' is already in use
  • KTOR-8924 Curl: Client sends both Transfer-Encoding and Content-Length headers for DELETE requests with body
  • KTOR-8838 Exception handling issue in client cache

3.3.2

Published 5 November 2025

Improvements

  • WebRTC Client. Remove redundant targets (KTOR-9062)
  • Add Socks proxy support to Darwin engine (KTOR-8968)
  • Java: Improve error message when SOCKS proxy is used (KTOR-2908)

Bugfixes

  • HttpRequestRetry: SendCountExceedException when max retries is more than maxSendCount of HttpSend (KTOR-5850)
  • Darwin: The maxFrameSize option has no effect (KTOR-6963)
  • OpenAPI: StackOverflowError when a response object has property with @Contextual serializer (KTOR-8878)
  • OpenAPI gen: missing KDoc fields (KTOR-9021)
  • Server call.request.path() returns routing selectors in path (KTOR-7639)
  • StaticContent doesn't allow siblings (KTOR-9012)
  • HttpCache: FileStorage doesn't use given dispatcher for all file operations (KTOR-8832)
  • Curl: SOCKS proxy doesn't work (KTOR-9008)
  • Netty: java.lang.VerifyError is thrown on Android since 3.3.0 (KTOR-8916)
  • Response body channel is canceled while the body is being saved when having HttpRequestRetry and onDownload (KTOR-8975)
  • HttpCache: InvalidCacheStateException when varyKeys stored in files contain uppercase letters since 3.3.0 (KTOR-8970)

3.3.1

Published 8 October 2025

Improvements

  • Add a note about SSE session lifetime in KDoc (KTOR-8440)
  • Update Kotlin to 2.2.20 (KTOR-8896)

Bugfixes

  • NumberFormatException when Content-Length header value contains null bytes (KTOR-4828)

... (truncated)

Changelog

Sourced from io.ktor:ktor-bom's changelog.

3.3.3

Published 26 November 2025

Improvements

  • KTOR-6837 Discrepancies when parsing URL host with CIO and Darwin engines compared to the rest engines
  • KTOR-9050 Logging: SimpleLogger should be an object, not a class
  • KTOR-9094 Jetty Client: Support HTTP/2 over cleartext (h2c)
  • KTOR-9120 OpenAPI gen: missing operationId for KDoc fields
  • KTOR-3019 Improve logging for CORS plugin

Bugfixes

  • KTOR-8671 Netty: RejectedExecutionException during shutdown on MacOS when dev mode is enabled
  • KTOR-9096 Darwin: New SSE handlers stop responding after canceling few SSE sessions
  • KTOR-9125 Double ResponseSent invocation when exception is thrown after respond
  • KTOR-8878 OpenAPI: StackOverflowError when a response object has property with @​Contextual serializer
  • KTOR-8947 Java, ContentEncoding: IllegalHeaderNameException is thrown for ":status" pseudo header with HTTP/2
  • KTOR-9092 NettyHttp2Handler throws IllegalArgumentException: 'ktor.ApplicationCall' is already in use
  • KTOR-8924 Curl: Client sends both Transfer-Encoding and Content-Length headers for DELETE requests with body
  • KTOR-8838 Exception handling issue in client cache

3.3.2

Published 5 November 2025

Improvements

  • WebRTC Client. Remove redundant targets (KTOR-9062)
  • Add Socks proxy support to Darwin engine (KTOR-8968)
  • Java: Improve error message when SOCKS proxy is used (KTOR-2908)

Bugfixes

  • HttpRequestRetry: SendCountExceedException when max retries is more than maxSendCount of HttpSend (KTOR-5850)
  • Darwin: The maxFrameSize option has no effect (KTOR-6963)
  • OpenAPI: StackOverflowError when a response object has property with @​Contextual serializer (KTOR-8878)
  • OpenAPI gen: missing KDoc fields (KTOR-9021)
  • Server call.request.path() returns routing selectors in path (KTOR-7639)
  • StaticContent doesn't allow siblings (KTOR-9012)
  • HttpCache: FileStorage doesn't use given dispatcher for all file operations (KTOR-8832)
  • Curl: SOCKS proxy doesn't work (KTOR-9008)
  • Netty: java.lang.VerifyError is thrown on Android since 3.3.0 (KTOR-8916)
  • Response body channel is canceled while the body is being saved when having HttpRequestRetry and onDownload (KTOR-8975)
  • HttpCache: InvalidCacheStateException when varyKeys stored in files contain uppercase letters since 3.3.0 (KTOR-8970)

3.3.1

Published 8 October 2025

Improvements

  • Add a note about SSE session lifetime in KDoc (KTOR-8440)
  • Update Kotlin to 2.2.20 (KTOR-8896)

... (truncated)

Commits
  • 629d22c Release 3.3.3 (#5219)
  • 77bbf10 Server Netty. Fix rejected execution during engine stop (#8671)
  • 75439b0 KTOR-9096 Fix SSE cancellation leak on Darwin (#5208)
  • 62ea8e4 KTOR-6837 Fix client URL handling for invalid DefaultRequest.host
  • ef09e84 Add stream.demand calls to JettyResponseListener
  • 47e6049 KTOR-9094 Jetty Client: Allow h2c traffic
  • e70922e KTOR-9125 Fix double ResponseSent invocation when exception is thrown after r...
  • 589f02d KTOR-3019 Improve CORS plugin logging to ease troubleshooting (#5194)
  • 5a66b47 Update Gradle and Develocity (#5201)
  • 2ba2f02 KTOR-8947 Filter pseudo-headers in Java client (#5198)
  • Additional commits viewable in compare view

Most Recent Ignore Conditions Applied to This Pull Request | Dependency Name | Ignore Conditions | | --- | --- | | io.ktor:ktor-bom | [>= 3.4.a, < 3.5] |
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 130a75b2d..f56c437ea 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] anthropic = "2.19.0" kotlin = "2.2.21" -ktor = "3.2.3" +ktor = "3.3.3" mcp-kotlin = "0.11.0" shadow = "9.4.1" slf4j = "2.0.17" From 8a7f9f8d31e4bb222a0a39858b1d5d598b69a788 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Fri, 10 Apr 2026 13:51:03 +0200 Subject: [PATCH 080/187] fix(server): GET SSE stream crash on Netty in Streamable HTTP (#681) `appendSseHeaders()` was called inside Ktor's `sse {}` block where response headers are already committed. On Netty this throws `UnsupportedOperationException`, killing the SSE stream and causing the client to retry in a loop ## How Has This Been Tested? unit, simple-streamable-server sample ## Breaking Changes none ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x ] I have added appropriate error handling - [ x] I have added or updated documentation as needed --- .../kotlin/sdk/server/KtorServer.kt | 14 ++++++ .../server/StreamableHttpServerTransport.kt | 8 ++-- .../StreamableHttpServerTransportTest.kt | 46 +++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) 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 35dcc7ecc..806a47916 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 @@ -3,13 +3,17 @@ package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.server.application.Application import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.ApplicationCallPipeline import io.ktor.server.application.MissingApplicationPluginException import io.ktor.server.application.install import io.ktor.server.request.ApplicationRequest import io.ktor.server.request.header +import io.ktor.server.request.httpMethod +import io.ktor.server.response.header import io.ktor.server.response.respond import io.ktor.server.routing.Route import io.ktor.server.routing.RoutingContext @@ -114,6 +118,16 @@ private fun Application.mcpStreamableHttp( routing { route(path) { + // Set Mcp-Session-Id on GET responses before Ktor's sse {} commits headers. + intercept(ApplicationCallPipeline.Plugins) { + if (context.request.httpMethod == HttpMethod.Get) { + val sessionId = context.request.header(MCP_SESSION_ID_HEADER) + if (sessionId != null && transportManager.getTransport(sessionId) != null) { + context.response.header(MCP_SESSION_ID_HEADER, sessionId) + } + } + } + sse { val transport = existingStreamableTransport(call, transportManager) ?: return@sse transport.handleRequest(this, call) diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 70948d9ce..17e6a282c 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -453,8 +453,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat return } - call.appendSseHeaders() - flushSse(sseSession) // flush headers immediately + // SSE headers (Content-Type, Cache-Control, Connection) are already set by the framework's SSE handler + flushSse(sseSession) streamsMapping[STANDALONE_SSE_STREAM_ID] = SessionContext(sseSession, call) maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession, call.request.header(MCP_PROTOCOL_VERSION_HEADER)) sseSession.coroutineContext.job.invokeOnCompletion { @@ -529,8 +529,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - call.appendSseHeaders() - flushSse(session) // flush headers immediately + // SSE headers are already set by the framework's SSE handler. + flushSse(session) val streamId = store.replayEventsAfter(lastEventId) { eventId, message -> try { diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt index 9cf916afb..8b4f6fae5 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt @@ -2,6 +2,7 @@ package io.modelcontextprotocol.kotlin.sdk.server import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.ktor.client.HttpClient import io.ktor.client.call.body @@ -10,7 +11,9 @@ import io.ktor.client.plugins.logging.Logging import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.header import io.ktor.client.request.post +import io.ktor.client.request.prepareGet import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsChannel import io.ktor.http.ContentType import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode @@ -21,6 +24,7 @@ import io.ktor.server.routing.post import io.ktor.server.routing.routing import io.ktor.server.testing.ApplicationTestBuilder import io.ktor.server.testing.testApplication +import io.ktor.utils.io.readUTF8Line import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation @@ -36,6 +40,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult import io.modelcontextprotocol.kotlin.sdk.types.McpJson import io.modelcontextprotocol.kotlin.sdk.types.Method import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.Tool import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import io.modelcontextprotocol.kotlin.sdk.types.toJSON @@ -433,6 +438,47 @@ class StreamableHttpServerTransportTest { } } + @Test + fun `GET SSE stream includes Mcp-Session-Id header and stays open`() = testApplication { + val mcpPath = "/mcp" + + application { + mcpStreamableHttp(mcpPath) { + Server( + Implementation("test-server", "1.0.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + } + } + + val client = createTestClient() + + // Step 1: Initialize session via POST + val initResponse = client.post(mcpPath) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = assertNotNull(initResponse.headers[MCP_SESSION_ID_HEADER]) + + // Step 2: Open GET SSE stream with session ID + client.prepareGet(mcpPath) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(MCP_SESSION_ID_HEADER, sessionId) + header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) + }.execute { response -> + // Verify Mcp-Session-Id is present on the SSE response + response.status shouldBe HttpStatusCode.OK + response.headers[MCP_SESSION_ID_HEADER] shouldBe sessionId + + // Verify the stream is alive by reading at least one line (flush event) + val channel = response.bodyAsChannel() + val firstLine = channel.readUTF8Line() + firstLine.shouldNotBeNull() + channel.isClosedForRead shouldBe false + } + } + private fun ApplicationTestBuilder.configureTransportEndpoint(transport: StreamableHttpServerTransport) { application { routing { From f02f5d3d10694c8fffd6a25b60f384a16b0441b9 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Fri, 10 Apr 2026 15:30:20 +0200 Subject: [PATCH 081/187] release: bump mcp kotlin-sdk version to 0.11.1 (#682) mcp kotlin-sdk 0.11.0 -> 0.11.1 ## Breaking Changes none ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 09a658be2..ca4a217ed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,4 +8,4 @@ kotlin.daemon.jvmargs=-Xmx4G kotlin.native.ignoreDisabledTargets=true group=io.modelcontextprotocol -version=0.11.0 +version=0.11.1 From 36c0f4f8966911c59c43a009b01f58c15f885c02 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:24:54 +0200 Subject: [PATCH 082/187] chore(deps): bump @types/node from 25.5.2 to 25.6.0 in /integration-test/src/jvmTest/typescript in the all-dependencies group (#690) Bumps the all-dependencies group in /integration-test/src/jvmTest/typescript with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 25.5.2 to 25.6.0
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/node&package-manager=npm_and_yarn&previous-version=25.5.2&new-version=25.6.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/jvmTest/typescript/package-lock.json | 16 ++++++++-------- .../src/jvmTest/typescript/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index dd0c9b4a3..a246efaf5 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -15,7 +15,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.5.2", + "@types/node": "^25.6.0", "cors": "^2.8.6", "tsx": "^4.21.0", "typescript": "^6.0.2" @@ -579,13 +579,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", - "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/qs": { @@ -1751,9 +1751,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true, "license": "MIT" }, diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index 20249d035..e38cca6e8 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.5.2", + "@types/node": "^25.6.0", "cors": "^2.8.6", "tsx": "^4.21.0", "typescript": "^6.0.2" From 1734a647c47c765a9962cf591dd9f6b3a281267d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:46:48 +0200 Subject: [PATCH 083/187] chore(deps): bump dependabot/fetch-metadata from 2 to 3 in the all-actions group across 1 directory (#689) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-actions group with 1 update in the / directory: [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata). Updates `dependabot/fetch-metadata` from 2 to 3
Release notes

Sourced from dependabot/fetch-metadata's releases.

v3.0.0

The breaking change is requiring Node.js version v24 as the Actions runtime.

What's Changed

New Contributors

Full Changelog: https://github.com/dependabot/fetch-metadata/compare/v2...v3.0.0

v2.5.0

What's Changed

... (truncated)

Commits
  • ffa630c v3.0.0 (#686)
  • ec8fff2 Merge pull request #674 from dependabot/dependabot/npm_and_yarn/picomatch-2.3.2
  • caf48bd build(deps-dev): bump picomatch from 2.3.1 to 2.3.2
  • 13d8274 Upgrade @​actions/github to ^9.0.0 and @​octokit/request-error to ^7.1.0 (#678)
  • b603099 Upgrade @​actions/core from ^1.11.1 to ^3.0.0 (#677)
  • c5dc5b1 Enable noImplicitAny in tsconfig.json (#684)
  • a183f3c Add typecheck step to CI (#685)
  • 5e17564 Remove skipLibCheck from tsconfig.json (#683)
  • bb56eeb Switch tsconfig module resolution to bundler (#682)
  • 3632e3d Remove vestigial outDir from tsconfig.json (#681)
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 6073684e7..e7e94c3b7 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -11,7 +11,7 @@ jobs: if: github.actor == 'dependabot[bot]' runs-on: ubuntu-latest steps: - - uses: dependabot/fetch-metadata@v2 + - uses: dependabot/fetch-metadata@v3 id: metadata - if: >- From c18a4887856f305cb34eb86ca23828a1fefb1d9c Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 13 Apr 2026 13:43:48 +0200 Subject: [PATCH 084/187] fix(ci): add node setup in release workflow (#684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added `setup-node` with Node.js 22 to the release workflow — without it, TypeScript integration tests fail because the macOS runner has an incompatible pre-installed Node.js version --- .github/workflows/gradle-publish.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index 6b60e8739..6ed075651 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -24,11 +24,16 @@ jobs: java-version: '21' distribution: 'temurin' + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed + - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 - - name: Build - run: ./gradlew build --rerun-tasks --max-workers 2 --continue + - name: Check + run: ./gradlew check --no-configuration-cache - name: Publish to Maven Central Portal id: publish From 1d02ae8a7ff28ffa23e7b8b54c6dc235bef43d94 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 13 Apr 2026 14:15:00 +0200 Subject: [PATCH 085/187] chore(build): remove openapi generator (#683) Remove unused OpenAPI Generator plugin, its configuration, and the `.openapi-generator-ignore` file. ## Breaking Changes none ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- gradle/libs.versions.toml | 2 - kotlin-sdk-core/.openapi-generator-ignore | 29 ------------ kotlin-sdk-core/build.gradle.kts | 56 ----------------------- 3 files changed, 87 deletions(-) delete mode 100644 kotlin-sdk-core/.openapi-generator-ignore diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2836ef7f..0fb313501 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,6 @@ ktlint = "14.2.0" knit = "0.5.1" mavenPublish = "0.36.0" netty = "4.2.12.Final" -openapi-generator = "7.21.0" # libraries version awaitility = "4.3.0" @@ -81,4 +80,3 @@ kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-co kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } knit = { id = "org.jetbrains.kotlinx.knit", version.ref = "knit" } -openapi-generator = { id = "org.openapi.generator", version.ref = "openapi-generator" } diff --git a/kotlin-sdk-core/.openapi-generator-ignore b/kotlin-sdk-core/.openapi-generator-ignore deleted file mode 100644 index dbdf93035..000000000 --- a/kotlin-sdk-core/.openapi-generator-ignore +++ /dev/null @@ -1,29 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -**/auth/** -**/infrastructure/** -!**/Base64ByteArray.kt -!**/Bytes.kt - - -## Exclude Models which are causing problems -**/JSONRPCResponse.kt -**/Result.kt -**/ListToolsResult.kt -**/InitializeRequest.kt -**/InitializeResult.kt -**/ServerCapabilities.kt -**/InitializeRequestParams.kt -**/CreateMessageRequest.kt -**/CreateMessageRequestParams.kt -**/ClientCapabilities.kt -**/Tool.kt -**/ToolInputSchema.kt -**/ToolOutputSchema.kt - diff --git a/kotlin-sdk-core/build.gradle.kts b/kotlin-sdk-core/build.gradle.kts index 0e702c476..f4a231483 100644 --- a/kotlin-sdk-core/build.gradle.kts +++ b/kotlin-sdk-core/build.gradle.kts @@ -7,62 +7,6 @@ plugins { id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) - alias(libs.plugins.openapi.generator) -} - -/* -tasks.withType>().configureEach { - dependsOn(tasks.openApiGenerate) -} - -tasks.named("runKtlintCheckOverCommonMainSourceSet") { - dependsOn(tasks.openApiGenerate) -} - -// Also ensure it runs before other relevant tasks -tasks.withType().configureEach { - dependsOn(tasks.openApiGenerate) -} - -tasks.withType().configureEach { - if (name.lowercase().contains("sourcesjar")) { - dependsOn(tasks.openApiGenerate) - } -} - */ - -openApiGenerate { - val schemaVersion = "2025-03-26" // or "2025-06-18" or "draft" - val schemaUrl = -// "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema/$schemaVersion/schema.json" - "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/tags/$schemaVersion/schema/$schemaVersion/schema.json" - generatorName = "kotlin" - remoteInputSpec = schemaUrl - outputDir = layout.buildDirectory.dir("generated-sources/openapi").get().asFile.absolutePath - packageName = "io.modelcontextprotocol.kotlin.sdk.models" - modelPackage = "io.modelcontextprotocol.kotlin.sdk.models" - apiPackage = "io.modelcontextprotocol.kotlin.sdk.api" - generateModelTests = false - generateModelDocumentation = false - cleanupOutput = false - skipValidateSpec = true // do not validate spec - library.set("multiplatform") - ignoreFileOverride.set("${layout.projectDirectory}/.openapi-generator-ignore") - globalProperties.set( - mapOf( - "supportingFiles" to "", - "models" to "", // or generate all models - ), - ) - configOptions.set( - mapOf( - "omitGradleWrapper" to "true", - "enumPropertyNaming" to "UPPERCASE", - "dateLibrary" to "kotlinx-datetime", - "explicitApi" to "true", - "modelMutable" to "false", - ), - ) } // Generation library versions From 6ce534dba92a60f7ef65008eea0c948908ad4bd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:46:26 +0200 Subject: [PATCH 086/187] chore(deps): bump com.anthropic:anthropic-java from 2.19.0 to 2.24.0 in /samples/kotlin-mcp-client in the other-dependencies group across 1 directory (#685) Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-client directory: [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java). Updates `com.anthropic:anthropic-java` from 2.19.0 to 2.24.0
Release notes

Sourced from com.anthropic:anthropic-java's releases.

v2.24.0

2.24.0 (2026-04-09)

Full Changelog: v2.23.0...v2.24.0

Features

v2.23.0

2.23.0 (2026-04-09)

Full Changelog: v2.22.0...v2.23.0

Features

  • api: Add beta advisor tool (17d360c)

Chores

  • client: add Models enum as option in Managed Agents (ebd7983)

v2.22.0

2.22.0 (2026-04-08)

Full Changelog: v2.21.0...v2.22.0

Features

  • api: add support for Claude Managed Agents (f410797)
  • bedrock: Create Bedrock Mantle client (#1066) (100dfb5)

Bug Fixes

  • client: fix examples for agents (7c11a93)
  • internal: run formatter (a2aa0c8)

v2.21.0

2.21.0 (2026-04-07)

Full Changelog: v2.20.0...v2.21.0

Features

  • api: Add support for claude-mythos-preview (b9ea13c)
  • vertex: add support for US multi-region endpoint (8e32b5f)

... (truncated)

Changelog

Sourced from com.anthropic:anthropic-java's changelog.

2.24.0 (2026-04-09)

Full Changelog: v2.23.0...v2.24.0

Features

2.23.0 (2026-04-09)

Full Changelog: v2.22.0...v2.23.0

Features

  • api: Add beta advisor tool (17d360c)

Chores

  • client: add Models enum as option in Managed Agents (ebd7983)

2.22.0 (2026-04-08)

Full Changelog: v2.21.0...v2.22.0

Features

  • api: add support for Claude Managed Agents (f410797)
  • bedrock: Create Bedrock Mantle client (#1066) (100dfb5)

Bug Fixes

  • client: fix examples for agents (7c11a93)
  • internal: run formatter (a2aa0c8)

2.21.0 (2026-04-07)

Full Changelog: v2.20.0...v2.21.0

Features

  • api: Add support for claude-mythos-preview (b9ea13c)
  • vertex: add support for US multi-region endpoint (8e32b5f)

2.20.0 (2026-04-01)

Full Changelog: v2.19.0...v2.20.0

... (truncated)

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index f56c437ea..9578551dc 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -anthropic = "2.19.0" +anthropic = "2.24.0" kotlin = "2.2.21" ktor = "3.3.3" mcp-kotlin = "0.11.0" From 081a635bd35a60a1f461999603fe1ad9a53582a4 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 14 Apr 2026 13:14:30 +0200 Subject: [PATCH 087/187] chore(ci): remove codecov (#691) Remove Codecov configuration and CI steps for uploading coverage/test reports. ## Motivation and Context - Codecov integration relies on third-party credentials - Codecov has not demonstrated clear value for the project: - Coverage analysis is already handled by kover, which runs locally and in CI - PR reviews still require manual inspection of tests regardless of codecov reports - Codecov flaky test detection is unreliable, it marks 3325 out of all tests as flaky with similar failure rates, making the signal useless (https://app.codecov.io/gh/modelcontextprotocol/kotlin-sdk/tests) --- .github/workflows/build.yml | 16 ------------ codecov.yml | 51 ------------------------------------- context7.json | 1 - 3 files changed, 68 deletions(-) delete mode 100644 codecov.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08242f769..caaa3f0d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,22 +104,6 @@ jobs: report_paths: '**/test-results/**/TEST-*.xml' truncate_stack_traces: false - - name: Upload coverage reports to Codecov - if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} - uses: codecov/codecov-action@v6 - with: - token: ${{ secrets.CODECOV_TOKEN }} - report_type: coverage - directory: ./build/reports/kover - files: report.xml - - - name: Upload test results to Codecov - if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} - uses: codecov/codecov-action@v6 - with: - token: ${{ secrets.CODECOV_TOKEN }} - report_type: test_results - - name: Archive Maven Local Repository if: ${{ success() && matrix.os == 'ubuntu-latest' }} diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 9135aae1e..000000000 --- a/codecov.yml +++ /dev/null @@ -1,51 +0,0 @@ -# Codecov configuration for Mokksy and AI-Mocks -# https://docs.codecov.com/docs/codecov-yaml - -codecov: - require_ci_to_pass: yes - -coverage: - precision: 2 - round: down - status: - project: - default: - target: auto - threshold: 1% - patch: - default: - target: auto - threshold: 1% - -comment: - layout: "header, diff, flags, components" - behavior: default - require_changes: true - -component_management: - default_rules: - statuses: - - type: project - target: auto - individual_components: - - - component_id: kotlin-sdk-core - name: SDK Core - paths: - - kotlin-sdk-core/** - - - component_id: kotlin-sdk-client - name: SDK Client - paths: - - kotlin-sdk-client/** - - - component_id: kotlin-sdk-server - name: SDK Server - paths: - - kotlin-sdk-server/** - - - component_id: kotlin-sdk-testing - name: SDK Testing - paths: - - kotlin-sdk-testing/** - diff --git a/context7.json b/context7.json index a28c0fc67..5fde250e5 100644 --- a/context7.json +++ b/context7.json @@ -12,7 +12,6 @@ "CONTRIBUTING.md", "LICENSE", "SECURITY.md", - "codecov.yml", "gradlew", "gradlew.bat", ".editorconfig", From 61fab2ffe081b6074697610b4c7bcf30ebeba285 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 14 Apr 2026 13:44:19 +0200 Subject: [PATCH 088/187] feat: add $schema field to ToolSchema per SEP-1613 (#696) Add optional `$schema` field to `ToolSchema`, establishing JSON Schema 2020-12 as the default dialect per SEP-1613. closes #422 ## How Has This Been Tested? unit tests ## Breaking Changes none ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- kotlin-sdk-core/api/kotlin-sdk-core.api | 16 +++--- .../kotlin/sdk/types/tools.kt | 8 +++ .../kotlin/sdk/types/ToolsTest.kt | 55 +++++++++++++++++++ 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index d4f88ba47..679b68d27 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -5135,17 +5135,19 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotif public final class io/modelcontextprotocol/kotlin/sdk/types/ToolSchema { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema$Companion; public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; - public final fun component2 ()Ljava/util/List; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; + public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;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 component3 ()Ljava/util/List; + public final fun component4 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema; public fun equals (Ljava/lang/Object;)Z public final fun getDefs ()Lkotlinx/serialization/json/JsonObject; public final fun getProperties ()Lkotlinx/serialization/json/JsonObject; public final fun getRequired ()Ljava/util/List; + public final fun getSchema ()Ljava/lang/String; public final fun getType ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt index 4f642f44a..a03e8ad2a 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt @@ -39,12 +39,14 @@ public fun CallToolResult.Companion.error(content: String, meta: JsonObject? = n * if both [title] and [annotations].[title] are not provided. * @property inputSchema A JSON Schema object defining the expected parameters for the tool. * Must be an object type schema. Defines what arguments the tool accepts. + * Defaults to the JSON Schema 2020-12 dialect when no explicit `$schema` is provided. * @property description A human-readable description of the tool and when to use it. * Clients can use this to improve the LLM's understanding of available tools. * It can be thought of like a "hint" to the model. * @property outputSchema An optional JSON Schema object defining the structure of the tool's output * returned in the [structuredContent][CallToolResult.structuredContent] field of a [CallToolResult]. * Must be an object type schema if provided. + * Defaults to the JSON Schema 2020-12 dialect when no explicit `$schema` is provided. * @property title Optional human-readable display name for this tool. * Intended for UI and end-user contexts, optimized to be easily understood * even by those unfamiliar with domain-specific terminology. @@ -77,7 +79,11 @@ public data class Tool( * A JSON Schema for tool input or output parameters. * * This is a simplified schema structure that must be of type "object". + * Defaults to the JSON Schema 2020-12 dialect when no explicit [schema] (`$schema`) is provided. * + * @property schema Optional URI identifying the JSON Schema dialect (e.g., + * `https://json-schema.org/draft/2020-12/schema`). Serialized as `$schema`. + * When absent, JSON Schema 2020-12 is assumed by default. * @property type Always "object" for tool schemas. * @property properties Optional map of property names to their schema definitions. * @property required Optional list of property names that are required. @@ -85,6 +91,8 @@ public data class Tool( */ @Serializable public data class ToolSchema( + @SerialName("\$schema") + val schema: String? = null, val properties: JsonObject? = null, val required: List? = null, @SerialName("\$defs") diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt index 543e12d16..87f1644c7 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt @@ -158,6 +158,61 @@ class ToolsTest { assertEquals("language", tool.meta?.get("category")?.jsonPrimitive?.content) } + @Test + fun `should serialize ToolSchema with schema field`() { + val tool = Tool( + name = "typed-tool", + inputSchema = ToolSchema( + schema = "https://json-schema.org/draft/2020-12/schema", + properties = buildJsonObject { + put("name", buildJsonObject { put("type", "string") }) + }, + ), + ) + + verifySerialization( + tool, + McpJson, + $$""" + { + "name": "typed-tool", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ToolSchema with schema field`() { + val json = $$""" + { + "name": "typed-tool", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + """.trimIndent() + + val tool = verifyDeserialization(McpJson, json) + + assertEquals("https://json-schema.org/draft/2020-12/schema", tool.inputSchema.schema) + assertNotNull(tool.inputSchema.properties) + } + @Test fun `should serialize ToolSchema with defs`() { val tool = toolWithDefs() From e74ef2db6fbfeb56888aa5ed495361960f5885e5 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 14 Apr 2026 15:27:32 +0200 Subject: [PATCH 089/187] feat(server): add tool name validation per SEP-986 (#695) Add warn-only validation for tool names at registration time, following the MCP tool naming standard (SEP-986) closes #417 ## How Has This Been Tested? unit tests ## Breaking Changes none ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .../kotlin/sdk/server/ServerToolsTest.kt | 10 ++ .../kotlin/sdk/server/Server.kt | 4 +- .../sdk/server/utils/ToolNameValidation.kt | 60 +++++++++ .../server/utils/ToolNameValidationTest.kt | 123 ++++++++++++++++++ 4 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidation.kt create mode 100644 kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidationTest.kt diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsTest.kt index c2e820153..54c998e94 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsTest.kt @@ -98,6 +98,16 @@ class ServerToolsTest : AbstractServerFeaturesTest() { assertFalse(toolListChangedNotificationReceived, "No notification should be sent when tool doesn't exist") } + @Test + fun `addTool should succeed with non-conforming tool name`() = runTest { + server.addTool("my invalid tool!", "Tool with non-conforming name", ToolSchema()) { + CallToolResult(listOf(TextContent("It works"))) + } + + val result = server.removeTool("my invalid tool!") + assertTrue(result, "Tool with non-conforming name should have been registered and removable") + } + @Test fun `removeTool should throw when tools capability is not supported`() = runTest { var toolListChangedNotificationReceived = false diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index de219d4e6..d9b9b9a78 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging +import io.modelcontextprotocol.kotlin.sdk.server.utils.warnIfInvalidToolName import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions import io.modelcontextprotocol.kotlin.sdk.shared.Transport @@ -303,7 +304,7 @@ public open class Server( logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" } "Server does not support tools capability. Enable it in ServerOptions." } - + warnIfInvalidToolName(tool.name) toolRegistry.add(RegisteredTool(tool, handler)) } @@ -357,6 +358,7 @@ public open class Server( logger.error { "Failed to add tools: Server does not support tools capability" } "Server does not support tools capability." } + toolsToAdd.forEach { warnIfInvalidToolName(it.tool.name) } toolRegistry.addAll(toolsToAdd) } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidation.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidation.kt new file mode 100644 index 000000000..a63c2e45b --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidation.kt @@ -0,0 +1,60 @@ +package io.modelcontextprotocol.kotlin.sdk.server.utils + +import io.github.oshai.kotlinlogging.KotlinLogging + +private val logger = KotlinLogging.logger("ToolNameValidation") + +private const val MAX_TOOL_NAME_LENGTH = 128 + +private const val SEP_986_URL = "https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986" + +/** + * Validates a tool name against the MCP tool naming standard (SEP-986). + * Returns a list of warnings. An empty list means the name is valid. + */ +internal fun validateToolName(name: String): List { + if (name.isEmpty()) return listOf("Tool name cannot be empty") + + val invalidChars = name.toSet().filterNot { it.isAsciiAllowed() } + val remainingInvalidChars = invalidChars - setOf(' ', ',') + + return listOfNotNull( + "Tool name exceeds maximum length of $MAX_TOOL_NAME_LENGTH characters (current: ${name.length})" + .takeIf { name.length > MAX_TOOL_NAME_LENGTH }, + "Tool name contains spaces, which may cause parsing issues" + .takeIf { ' ' in invalidChars }, + "Tool name contains commas, which may cause parsing issues" + .takeIf { ',' in invalidChars }, + "Tool name contains invalid characters: ${remainingInvalidChars.joinToString(", ") { "\"$it\"" }}" + .takeIf { remainingInvalidChars.isNotEmpty() }, + "Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)" + .takeIf { invalidChars.isNotEmpty() }, + "Tool name starts or ends with a dash, which may cause parsing issues in some contexts" + .takeIf { name.startsWith('-') || name.endsWith('-') }, + "Tool name starts or ends with a dot, which may cause parsing issues in some contexts" + .takeIf { name.startsWith('.') || name.endsWith('.') }, + ) +} + +/** + * Validates the tool [name] and logs a warning if it does not conform to SEP-986. + * Does not block registration — tools with non-conforming names are still accepted. + */ +internal fun warnIfInvalidToolName(name: String) { + val warnings = validateToolName(name) + if (warnings.isEmpty()) return + + val warningLines = warnings.joinToString("\n") { " - $it" } + logger.warn { + """ + |Tool name validation warning for "$name": + |$warningLines + |Tool registration will proceed, but this may cause compatibility issues. + |Consider updating the tool name to conform to the MCP tool naming standard. + |See SEP-986: $SEP_986_URL + """.trimMargin() + } +} + +private fun Char.isAsciiAllowed(): Boolean = + this in 'a'..'z' || this in 'A'..'Z' || this in '0'..'9' || this == '_' || this == '-' || this == '.' diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidationTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidationTest.kt new file mode 100644 index 000000000..f6760eeaa --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/utils/ToolNameValidationTest.kt @@ -0,0 +1,123 @@ +package io.modelcontextprotocol.kotlin.sdk.server.utils + +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.collections.shouldNotBeEmpty +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class ToolNameValidationTest { + + @ParameterizedTest + @ValueSource( + strings = [ + "getUser", + "get_user_profile", + "user-profile-update", + "admin.tools.list", + "DATA_EXPORT_v2.1", + "a", + "Z", + "0", + "a-b.c_d", + ], + ) + fun `should return no warnings for valid tool names`(name: String) { + validateToolName(name).shouldBeEmpty() + } + + @Test + fun `should return no warnings for max length name`() { + validateToolName("a".repeat(128)).shouldBeEmpty() + } + + @Test + fun `should warn for empty name`() { + val warnings = validateToolName("") + warnings shouldHaveSize 1 + warnings shouldContain "Tool name cannot be empty" + } + + @Test + fun `should warn for name exceeding max length`() { + val warnings = validateToolName("a".repeat(129)) + warnings.shouldNotBeEmpty() + warnings.any { "exceeds maximum length" in it }.shouldBeTrue() + } + + @ParameterizedTest + @ValueSource(strings = ["get user profile", "my tool"]) + fun `should warn for names with spaces`(name: String) { + val warnings = validateToolName(name) + warnings shouldContain "Tool name contains spaces, which may cause parsing issues" + } + + @ParameterizedTest + @ValueSource(strings = ["get,user,profile", "a,b"]) + fun `should warn for names with commas`(name: String) { + val warnings = validateToolName(name) + warnings shouldContain "Tool name contains commas, which may cause parsing issues" + } + + @ParameterizedTest + @ValueSource(strings = ["user/profile/update", "a/b"]) + fun `should warn for names with forward slashes`(name: String) { + val warnings = validateToolName(name) + warnings.any { "invalid characters" in it }.shouldBeTrue() + } + + @ParameterizedTest + @ValueSource(strings = ["user@domain.com", "tool#1", "tool\$name"]) + fun `should warn for names with special characters`(name: String) { + val warnings = validateToolName(name) + warnings.any { "invalid characters" in it }.shouldBeTrue() + warnings shouldContain "Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)" + } + + @Test + fun `should warn for unicode characters`() { + val warnings = validateToolName("user-\u00f1ame") + warnings.any { "invalid characters" in it }.shouldBeTrue() + } + + @Test + fun `should warn for name starting with dash`() { + val warnings = validateToolName("-my-tool") + warnings shouldContain "Tool name starts or ends with a dash, which may cause parsing issues in some contexts" + } + + @Test + fun `should warn for name ending with dash`() { + val warnings = validateToolName("my-tool-") + warnings shouldContain "Tool name starts or ends with a dash, which may cause parsing issues in some contexts" + } + + @Test + fun `should warn for name starting with dot`() { + val warnings = validateToolName(".hidden") + warnings shouldContain "Tool name starts or ends with a dot, which may cause parsing issues in some contexts" + } + + @Test + fun `should warn for name ending with dot`() { + val warnings = validateToolName("config.") + warnings shouldContain "Tool name starts or ends with a dot, which may cause parsing issues in some contexts" + } + + @Test + fun `should produce multiple warnings for multiple issues`() { + val warnings = validateToolName("my tool,name") + warnings.any { "spaces" in it }.shouldBeTrue() + warnings.any { "commas" in it }.shouldBeTrue() + } + + @Test + fun `should not duplicate space or comma in generic invalid characters message`() { + val warnings = validateToolName("my tool") + warnings shouldContain "Tool name contains spaces, which may cause parsing issues" + warnings.none { it.startsWith("Tool name contains invalid characters") }.shouldBeTrue() + } +} From 2fb3393d54092ece319431f79e81877c3d0fbdf9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:26:04 +0200 Subject: [PATCH 090/187] chore(deps): bump com.anthropic:anthropic-java from 2.24.0 to 2.25.0 in /samples/kotlin-mcp-client in the other-dependencies group across 1 directory (#698) Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-client directory: [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java). Updates `com.anthropic:anthropic-java` from 2.24.0 to 2.25.0
Release notes

Sourced from com.anthropic:anthropic-java's releases.

v2.25.0

2.25.0 (2026-04-14)

Full Changelog: v2.24.0...v2.25.0

Features

  • api: mark Sonnet and Opus 4 as deprecated (81c750e)

Bug Fixes

  • streaming: add missing events (4ac1b6a)

Chores

  • client: reset .stats.yml (1d409ea)

Documentation

Changelog

Sourced from com.anthropic:anthropic-java's changelog.

2.25.0 (2026-04-14)

Full Changelog: v2.24.0...v2.25.0

Features

  • api: mark Sonnet and Opus 4 as deprecated (81c750e)

Bug Fixes

  • streaming: add missing events (4ac1b6a)

Chores

  • client: reset .stats.yml (1d409ea)

Documentation

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.anthropic:anthropic-java&package-manager=gradle&previous-version=2.24.0&new-version=2.25.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 9578551dc..0db8b9f01 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -anthropic = "2.24.0" +anthropic = "2.25.0" kotlin = "2.2.21" ktor = "3.3.3" mcp-kotlin = "0.11.0" From 9981a7b67aec290cf4382f4268f4b5d7798cb208 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:49:49 +0200 Subject: [PATCH 091/187] chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.11.0 to 0.11.1 in /samples/kotlin-mcp-client (#699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [io.modelcontextprotocol:kotlin-sdk-client](https://github.com/modelcontextprotocol/kotlin-sdk) from 0.11.0 to 0.11.1.
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.11.1

Description

Fixes an SSE stream crash on Netty when clients connect to the Streamable HTTP GET endpoint.

Fixed

  • Fixed GET SSE stream crash on Netty in Streamable HTTP where appendSseHeaders() was called after response headers were already committed, causing UnsupportedOperationException and client retry loops by @​devcrocod in #681

Dependencies

  • mcp-kotlin to v0.11.0 in #672, #673, #675, #676
  • Ktor BOM to v3.3.3 in #650, #653, #656
  • Kotest assertions to v6.1.11 in #674
  • anthropic-java to v2.19.0 in #671
  • @​modelcontextprotocol/sdk to v1.29.0, @​types/node update in #677
  • hono to v4.12.12 in #680
  • @​hono/node-server to v1.19.13 in #679

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.0...0.11.1

Commits
  • f02f5d3 release: bump mcp kotlin-sdk version to 0.11.1 (#682)
  • 8a7f9f8 fix(server): GET SSE stream crash on Netty in Streamable HTTP (#681)
  • cceca4d chore(deps): bump io.ktor:ktor-bom from 3.2.3 to 3.3.3 in /samples/kotlin-mcp...
  • 12171dc chore(deps): bump hono from 4.12.9 to 4.12.12 in /integration-test/src/jvmTes...
  • 66d1795 chore(deps): bump @​hono/node-server from 1.19.11 to 1.19.13 in /integration-t...
  • 8442ab8 chore(deps): bump com.anthropic:anthropic-java from 2.18.0 to 2.19.0 in /samp...
  • 20ac69f chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/simple-streama...
  • 4144db6 chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.10.0 to 0....
  • 3f7f499 chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/kotlin-mcp-ser...
  • 7c87ed1 chore(deps): bump the kotest group with 3 updates (#674)
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 0db8b9f01..695b94dc0 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -2,7 +2,7 @@ anthropic = "2.25.0" kotlin = "2.2.21" ktor = "3.3.3" -mcp-kotlin = "0.11.0" +mcp-kotlin = "0.11.1" shadow = "9.4.1" slf4j = "2.0.17" From ba265a52ad1e59eae33abf940fbd944eb18d6333 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:28:47 +0000 Subject: [PATCH 092/187] chore(deps): bump mcp-kotlin from 0.11.0 to 0.11.1 in /samples/kotlin-mcp-server (#700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.11.0 to 0.11.1. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.11.0 to 0.11.1
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.11.1

Description

Fixes an SSE stream crash on Netty when clients connect to the Streamable HTTP GET endpoint.

Fixed

  • Fixed GET SSE stream crash on Netty in Streamable HTTP where appendSseHeaders() was called after response headers were already committed, causing UnsupportedOperationException and client retry loops by @​devcrocod in #681

Dependencies

  • mcp-kotlin to v0.11.0 in #672, #673, #675, #676
  • Ktor BOM to v3.3.3 in #650, #653, #656
  • Kotest assertions to v6.1.11 in #674
  • anthropic-java to v2.19.0 in #671
  • @​modelcontextprotocol/sdk to v1.29.0, @​types/node update in #677
  • hono to v4.12.12 in #680
  • @​hono/node-server to v1.19.13 in #679

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.0...0.11.1

Commits
  • f02f5d3 release: bump mcp kotlin-sdk version to 0.11.1 (#682)
  • 8a7f9f8 fix(server): GET SSE stream crash on Netty in Streamable HTTP (#681)
  • cceca4d chore(deps): bump io.ktor:ktor-bom from 3.2.3 to 3.3.3 in /samples/kotlin-mcp...
  • 12171dc chore(deps): bump hono from 4.12.9 to 4.12.12 in /integration-test/src/jvmTes...
  • 66d1795 chore(deps): bump @​hono/node-server from 1.19.11 to 1.19.13 in /integration-t...
  • 8442ab8 chore(deps): bump com.anthropic:anthropic-java from 2.18.0 to 2.19.0 in /samp...
  • 20ac69f chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/simple-streama...
  • 4144db6 chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.10.0 to 0....
  • 3f7f499 chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/kotlin-mcp-ser...
  • 7c87ed1 chore(deps): bump the kotest group with 3 updates (#674)
  • Additional commits viewable in compare view

Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.11.0 to 0.11.1
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

0.11.1

Description

Fixes an SSE stream crash on Netty when clients connect to the Streamable HTTP GET endpoint.

Fixed

  • Fixed GET SSE stream crash on Netty in Streamable HTTP where appendSseHeaders() was called after response headers were already committed, causing UnsupportedOperationException and client retry loops by @​devcrocod in #681

Dependencies

  • mcp-kotlin to v0.11.0 in #672, #673, #675, #676
  • Ktor BOM to v3.3.3 in #650, #653, #656
  • Kotest assertions to v6.1.11 in #674
  • anthropic-java to v2.19.0 in #671
  • @​modelcontextprotocol/sdk to v1.29.0, @​types/node update in #677
  • hono to v4.12.12 in #680
  • @​hono/node-server to v1.19.13 in #679

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.0...0.11.1

Commits
  • f02f5d3 release: bump mcp kotlin-sdk version to 0.11.1 (#682)
  • 8a7f9f8 fix(server): GET SSE stream crash on Netty in Streamable HTTP (#681)
  • cceca4d chore(deps): bump io.ktor:ktor-bom from 3.2.3 to 3.3.3 in /samples/kotlin-mcp...
  • 12171dc chore(deps): bump hono from 4.12.9 to 4.12.12 in /integration-test/src/jvmTes...
  • 66d1795 chore(deps): bump @​hono/node-server from 1.19.11 to 1.19.13 in /integration-t...
  • 8442ab8 chore(deps): bump com.anthropic:anthropic-java from 2.18.0 to 2.19.0 in /samp...
  • 20ac69f chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/simple-streama...
  • 4144db6 chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.10.0 to 0....
  • 3f7f499 chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/kotlin-mcp-ser...
  • 7c87ed1 chore(deps): bump the kotest group with 3 updates (#674)
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index 05007d0f7..3a03aed06 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.2.21" ktor = "3.3.3" -mcp-kotlin = "0.11.0" +mcp-kotlin = "0.11.1" slf4j = "2.0.17" shadow = "9.4.1" From 2d23827963c04dec67806e0011fcacdba5a90a07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 00:17:16 +0200 Subject: [PATCH 093/187] chore(deps): bump mcp-kotlin from 0.11.0 to 0.11.1 in /samples/simple-streamable-server (#701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.11.0 to 0.11.1. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.11.0 to 0.11.1
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.11.1

Description

Fixes an SSE stream crash on Netty when clients connect to the Streamable HTTP GET endpoint.

Fixed

  • Fixed GET SSE stream crash on Netty in Streamable HTTP where appendSseHeaders() was called after response headers were already committed, causing UnsupportedOperationException and client retry loops by @​devcrocod in #681

Dependencies

  • mcp-kotlin to v0.11.0 in #672, #673, #675, #676
  • Ktor BOM to v3.3.3 in #650, #653, #656
  • Kotest assertions to v6.1.11 in #674
  • anthropic-java to v2.19.0 in #671
  • @​modelcontextprotocol/sdk to v1.29.0, @​types/node update in #677
  • hono to v4.12.12 in #680
  • @​hono/node-server to v1.19.13 in #679

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.0...0.11.1

Commits
  • f02f5d3 release: bump mcp kotlin-sdk version to 0.11.1 (#682)
  • 8a7f9f8 fix(server): GET SSE stream crash on Netty in Streamable HTTP (#681)
  • cceca4d chore(deps): bump io.ktor:ktor-bom from 3.2.3 to 3.3.3 in /samples/kotlin-mcp...
  • 12171dc chore(deps): bump hono from 4.12.9 to 4.12.12 in /integration-test/src/jvmTes...
  • 66d1795 chore(deps): bump @​hono/node-server from 1.19.11 to 1.19.13 in /integration-t...
  • 8442ab8 chore(deps): bump com.anthropic:anthropic-java from 2.18.0 to 2.19.0 in /samp...
  • 20ac69f chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/simple-streama...
  • 4144db6 chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.10.0 to 0....
  • 3f7f499 chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/kotlin-mcp-ser...
  • 7c87ed1 chore(deps): bump the kotest group with 3 updates (#674)
  • Additional commits viewable in compare view

Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.11.0 to 0.11.1
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

0.11.1

Description

Fixes an SSE stream crash on Netty when clients connect to the Streamable HTTP GET endpoint.

Fixed

  • Fixed GET SSE stream crash on Netty in Streamable HTTP where appendSseHeaders() was called after response headers were already committed, causing UnsupportedOperationException and client retry loops by @​devcrocod in #681

Dependencies

  • mcp-kotlin to v0.11.0 in #672, #673, #675, #676
  • Ktor BOM to v3.3.3 in #650, #653, #656
  • Kotest assertions to v6.1.11 in #674
  • anthropic-java to v2.19.0 in #671
  • @​modelcontextprotocol/sdk to v1.29.0, @​types/node update in #677
  • hono to v4.12.12 in #680
  • @​hono/node-server to v1.19.13 in #679

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.0...0.11.1

Commits
  • f02f5d3 release: bump mcp kotlin-sdk version to 0.11.1 (#682)
  • 8a7f9f8 fix(server): GET SSE stream crash on Netty in Streamable HTTP (#681)
  • cceca4d chore(deps): bump io.ktor:ktor-bom from 3.2.3 to 3.3.3 in /samples/kotlin-mcp...
  • 12171dc chore(deps): bump hono from 4.12.9 to 4.12.12 in /integration-test/src/jvmTes...
  • 66d1795 chore(deps): bump @​hono/node-server from 1.19.11 to 1.19.13 in /integration-t...
  • 8442ab8 chore(deps): bump com.anthropic:anthropic-java from 2.18.0 to 2.19.0 in /samp...
  • 20ac69f chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/simple-streama...
  • 4144db6 chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.10.0 to 0....
  • 3f7f499 chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/kotlin-mcp-ser...
  • 7c87ed1 chore(deps): bump the kotest group with 3 updates (#674)
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/simple-streamable-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index e74e9df75..0cca0267f 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin = "2.2.21" ktor = "3.3.3" junit = "6.0.3" -mcp-kotlin = "0.11.0" +mcp-kotlin = "0.11.1" slf4j = "2.0.17" shadow = "9.4.1" From eb248035479d5c9ff9b44d1982d7b508fb185371 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:10:52 +0000 Subject: [PATCH 094/187] chore(deps): bump mcp-kotlin from 0.11.0 to 0.11.1 in /samples/weather-stdio-server (#702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.11.0 to 0.11.1. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.11.0 to 0.11.1
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.11.1

Description

Fixes an SSE stream crash on Netty when clients connect to the Streamable HTTP GET endpoint.

Fixed

  • Fixed GET SSE stream crash on Netty in Streamable HTTP where appendSseHeaders() was called after response headers were already committed, causing UnsupportedOperationException and client retry loops by @​devcrocod in #681

Dependencies

  • mcp-kotlin to v0.11.0 in #672, #673, #675, #676
  • Ktor BOM to v3.3.3 in #650, #653, #656
  • Kotest assertions to v6.1.11 in #674
  • anthropic-java to v2.19.0 in #671
  • @​modelcontextprotocol/sdk to v1.29.0, @​types/node update in #677
  • hono to v4.12.12 in #680
  • @​hono/node-server to v1.19.13 in #679

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.0...0.11.1

Commits
  • f02f5d3 release: bump mcp kotlin-sdk version to 0.11.1 (#682)
  • 8a7f9f8 fix(server): GET SSE stream crash on Netty in Streamable HTTP (#681)
  • cceca4d chore(deps): bump io.ktor:ktor-bom from 3.2.3 to 3.3.3 in /samples/kotlin-mcp...
  • 12171dc chore(deps): bump hono from 4.12.9 to 4.12.12 in /integration-test/src/jvmTes...
  • 66d1795 chore(deps): bump @​hono/node-server from 1.19.11 to 1.19.13 in /integration-t...
  • 8442ab8 chore(deps): bump com.anthropic:anthropic-java from 2.18.0 to 2.19.0 in /samp...
  • 20ac69f chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/simple-streama...
  • 4144db6 chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.10.0 to 0....
  • 3f7f499 chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/kotlin-mcp-ser...
  • 7c87ed1 chore(deps): bump the kotest group with 3 updates (#674)
  • Additional commits viewable in compare view

Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.11.0 to 0.11.1
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

0.11.1

Description

Fixes an SSE stream crash on Netty when clients connect to the Streamable HTTP GET endpoint.

Fixed

  • Fixed GET SSE stream crash on Netty in Streamable HTTP where appendSseHeaders() was called after response headers were already committed, causing UnsupportedOperationException and client retry loops by @​devcrocod in #681

Dependencies

  • mcp-kotlin to v0.11.0 in #672, #673, #675, #676
  • Ktor BOM to v3.3.3 in #650, #653, #656
  • Kotest assertions to v6.1.11 in #674
  • anthropic-java to v2.19.0 in #671
  • @​modelcontextprotocol/sdk to v1.29.0, @​types/node update in #677
  • hono to v4.12.12 in #680
  • @​hono/node-server to v1.19.13 in #679

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.0...0.11.1

Commits
  • f02f5d3 release: bump mcp kotlin-sdk version to 0.11.1 (#682)
  • 8a7f9f8 fix(server): GET SSE stream crash on Netty in Streamable HTTP (#681)
  • cceca4d chore(deps): bump io.ktor:ktor-bom from 3.2.3 to 3.3.3 in /samples/kotlin-mcp...
  • 12171dc chore(deps): bump hono from 4.12.9 to 4.12.12 in /integration-test/src/jvmTes...
  • 66d1795 chore(deps): bump @​hono/node-server from 1.19.11 to 1.19.13 in /integration-t...
  • 8442ab8 chore(deps): bump com.anthropic:anthropic-java from 2.18.0 to 2.19.0 in /samp...
  • 20ac69f chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/simple-streama...
  • 4144db6 chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.10.0 to 0....
  • 3f7f499 chore(deps): bump mcp-kotlin from 0.10.0 to 0.11.0 in /samples/kotlin-mcp-ser...
  • 7c87ed1 chore(deps): bump the kotest group with 3 updates (#674)
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index cc2b53a2c..839a283d8 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ coroutines = "1.10.2" kotlin = "2.2.21" ktor = "3.3.3" logging = "8.0.01" -mcp-kotlin = "0.11.0" +mcp-kotlin = "0.11.1" shadow = "9.4.1" slf4j = "2.0.17" From bce6811b66367eafbc36b37153ff3ded2bb36883 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 15 Apr 2026 14:12:05 +0200 Subject: [PATCH 095/187] fix: validate protocol version header on initialization requests (#697) Validate `mcp-protocol-version` HTTP header during `InitializeRequest` Fix `DEFAULT_NEGOTIATED_PROTOCOL_VERSION` to match the spec fixes #547 ## How Has This Been Tested? Updated existing test (removed TODO referencing #547) to verify init request ## Breaking Changes none ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .../kotlin/sdk/types/common.kt | 2 +- .../server/StreamableHttpServerTransport.kt | 41 ++++++++++--------- .../StreamableHttpServerTransportTest.kt | 30 ++++++++++++-- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt index ac867af1d..ca083568e 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt @@ -12,7 +12,7 @@ import kotlinx.serialization.json.JsonObject public const val LATEST_PROTOCOL_VERSION: String = "2025-11-25" /** The default protocol version used when negotiation is not performed. */ -public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-06-18" +public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-03-26" /** All MCP protocol versions supported by this SDK. */ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 17e6a282c..9c105f94f 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -367,6 +367,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat ) return } + if (!validateProtocolVersion(call)) return if (messages.size > 1) { call.reject( HttpStatusCode.BadRequest, @@ -393,11 +394,23 @@ public class StreamableHttpServerTransport(private val configuration: Configurat return } + // Extract protocol version for priming event decision. + // For initialize requests, get from request params. + // For other requests, get from header (already validated). + val clientProtocolVersion = if (isInitializationRequest) { + val initRequest = messages.first() as JSONRPCRequest + (initRequest.params as? JsonObject)?.get("protocolVersion") + ?.let { McpJson.decodeFromJsonElement(it) } + ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION + } else { + call.request.header(MCP_PROTOCOL_VERSION_HEADER) ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION + } + val streamId = Uuid.random().toString() if (!configuration.enableJsonResponse) { call.appendSseHeaders() flushSse(session) // flush headers immediately - maybeSendPrimingEvent(streamId, session, call.request.header(MCP_PROTOCOL_VERSION_HEADER)) + maybeSendPrimingEvent(streamId, session, clientProtocolVersion) } streamMutex.withLock { @@ -456,7 +469,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat // SSE headers (Content-Type, Cache-Control, Connection) are already set by the framework's SSE handler flushSse(sseSession) streamsMapping[STANDALONE_SSE_STREAM_ID] = SessionContext(sseSession, call) - maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession, call.request.header(MCP_PROTOCOL_VERSION_HEADER)) + val clientProtocolVersion = + call.request.header(MCP_PROTOCOL_VERSION_HEADER) ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION + maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession, clientProtocolVersion) sseSession.coroutineContext.job.invokeOnCompletion { streamsMapping.remove(STANDALONE_SSE_STREAM_ID) } @@ -568,9 +583,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat return false } - val sessionHeaderValues = call.request.headers.getAll(MCP_SESSION_ID_HEADER) + val headerId = call.request.header(MCP_SESSION_ID_HEADER) - if (sessionHeaderValues.isNullOrEmpty()) { + if (headerId == null) { call.reject( HttpStatusCode.BadRequest, RPCError.ErrorCode.CONNECTION_CLOSED, @@ -579,17 +594,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat return false } - if (sessionHeaderValues.size > 1) { - call.reject( - HttpStatusCode.BadRequest, - RPCError.ErrorCode.CONNECTION_CLOSED, - "Bad Request: Mcp-Session-Id header must be a single value", - ) - return false - } - - val headerId = sessionHeaderValues.single() - return when (headerId) { sessionId -> true @@ -605,8 +609,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } private suspend fun validateProtocolVersion(call: ApplicationCall): Boolean { - val protocolVersions = call.request.headers.getAll(MCP_PROTOCOL_VERSION_HEADER) - val version = protocolVersions?.lastOrNull() ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION + val version = call.request.headers[MCP_PROTOCOL_VERSION_HEADER] ?: return true return when (version) { !in SUPPORTED_PROTOCOL_VERSIONS -> { @@ -715,14 +718,14 @@ public class StreamableHttpServerTransport(private val configuration: Configurat private suspend fun maybeSendPrimingEvent( streamId: String, session: ServerSSESession?, - clientProtocolVersion: String? = null, + clientProtocolVersion: String, ) { val store = configuration.eventStore if (store == null || session == null) return // Priming events have empty data which older clients cannot handle. // Only send priming events to clients with protocol version >= 2025-11-25 // which includes the fix for handling empty SSE data. - if (clientProtocolVersion != null && clientProtocolVersion < MIN_PRIMING_EVENT_PROTOCOL_VERSION) return + if (clientProtocolVersion < MIN_PRIMING_EVENT_PROTOCOL_VERSION) return try { val primingEventId = store.storeEvent(streamId, JSONRPCEmptyMessage) session.send( diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt index 8b4f6fae5..d00e10ec5 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt @@ -176,6 +176,31 @@ class StreamableHttpServerTransportTest { secondResponse.status shouldBe HttpStatusCode.BadRequest } + @Test + fun `init request with unsupported protocol version returns an HTTP error`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + transport.onMessage { message -> + if (message is JSONRPCRequest) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + + configureTransportEndpoint(transport) + + val initResponse = client.post(path) { + addStreamableHeaders() + header("mcp-protocol-version", "1900-01-01") + setBody(buildInitializeRequestPayload()) + } + + initResponse.status shouldBe HttpStatusCode.BadRequest + initResponse.headers[MCP_SESSION_ID_HEADER] shouldBe null + } + @Test fun `request with unsupported protocol version returns an HTTP error`() = testApplication { configTestServer() @@ -191,18 +216,15 @@ class StreamableHttpServerTransportTest { configureTransportEndpoint(transport) - val initPayload = buildInitializeRequestPayload() val initResponse = client.post(path) { addStreamableHeaders() - setBody(initPayload) + setBody(buildInitializeRequestPayload()) } initResponse.status shouldBe HttpStatusCode.OK val sessionId = initResponse.headers[MCP_SESSION_ID_HEADER] assertNotNull(sessionId) - // TODO When https://github.com/modelcontextprotocol/kotlin-sdk/issues/547 is fixed, - // check the incompatible mcp-protocol-version in the InitializeRequest and delete the part below val response = client.post(path) { addStreamableHeaders() header("mcp-session-id", sessionId) From e7b09d84f74e2891e139c73c4d4bca4ca28d54ef Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 15 Apr 2026 14:59:01 +0200 Subject: [PATCH 096/187] fix: propagate CancellationException instead of logging as error (#706) fixes #242 ## How Has This Been Tested? new and existing tests pass ## Breaking Changes none ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .../kotlin/sdk/client/SseClientTransport.kt | 19 ++++--- .../kotlin/sdk/client/StdioClientTransport.kt | 10 +++- .../kotlin/sdk/shared/Protocol.kt | 5 ++ .../sdk/shared/WebSocketMcpTransport.kt | 4 ++ .../kotlin/sdk/shared/ProtocolTest.kt | 40 +++++++++++++++ .../kotlin/sdk/server/SSEServerTransport.kt | 6 +++ .../server/StreamableHttpServerTransport.kt | 50 +++++++++++++------ 7 files changed, 109 insertions(+), 25 deletions(-) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt index 3c9d88b86..a48db608d 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt @@ -24,11 +24,12 @@ import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.SerializationException import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.time.Duration @@ -171,13 +172,15 @@ public class SseClientTransport( } override suspend fun closeResources() { - job?.cancelAndJoin() - try { - if (::session.isInitialized) session.cancel() - if (::scope.isInitialized) scope.cancel() - endpoint.cancel() - } catch (e: Throwable) { - _onError(e) + withContext(NonCancellable) { + job?.cancel() + try { + if (::session.isInitialized) session.cancel() + if (::scope.isInitialized) scope.cancel() + endpoint.cancel() + } catch (e: Throwable) { + _onError(e) + } } } } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt index 12529cc1d..a61ea4bf3 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelAndJoin @@ -31,6 +32,7 @@ import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import kotlinx.io.Buffer import kotlinx.io.IOException @@ -241,8 +243,10 @@ public class StdioClientTransport @JvmOverloads public constructor( } override suspend fun closeResources() { - scope.stopProcessing("Closed") - scope.coroutineContext[Job]?.join() // Wait for all coroutines to complete + withContext(NonCancellable) { + scope.stopProcessing("Closed") + scope.coroutineContext[Job]?.join() // Wait for all coroutines to complete + } } private fun sendOutboundMessage(message: JSONRPCMessage, sink: Sink, mainScope: CoroutineScope) { @@ -264,6 +268,8 @@ public class StdioClientTransport @JvmOverloads public constructor( private suspend fun handleJSONRPCMessage(msg: JSONRPCMessage) { try { _onMessage.invoke(msg) + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { logger.error(e) { "Error processing message." } runCatching { _onError.invoke(e) } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 43c0ae583..bc3cf7673 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -277,12 +277,15 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } try { handler(notification) + } catch (e: CancellationException) { + throw e } catch (cause: Throwable) { logger.error(cause) { "Error handling notification: ${notification.method}" } onError(cause) } } + @Suppress("ThrowsCount") private suspend fun onRequest(request: JSONRPCRequest) { logger.trace { "Received request: ${request.method} (id: ${request.id})" } @@ -300,6 +303,8 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio ), ), ) + } catch (e: CancellationException) { + throw e } catch (cause: Throwable) { logger.error(cause) { "Error sending method not found response" } onError(cause) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt index 5f8bf9dac..9fa1a0ba0 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.coroutines.cancellation.CancellationException /** WebSocket subprotocol identifier for MCP connections. */ public const val MCP_SUBPROTOCOL: String = "mcp" @@ -44,6 +45,7 @@ public abstract class WebSocketMcpTransport : AbstractTransport() { */ protected abstract suspend fun initializeSession() + @Suppress("ThrowsCount") override suspend fun start() { logger.debug { "Starting websocket transport" } @@ -74,6 +76,8 @@ public abstract class WebSocketMcpTransport : AbstractTransport() { try { val message = McpJson.decodeFromString(message.readText()) _onMessage.invoke(message) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { _onError.invoke(e) throw e diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt index b228071a4..b726c2feb 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt @@ -1,11 +1,14 @@ package io.modelcontextprotocol.kotlin.sdk.shared +import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.types.CustomRequest import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse import io.modelcontextprotocol.kotlin.sdk.types.McpJson @@ -24,6 +27,7 @@ import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import kotlin.coroutines.cancellation.CancellationException import kotlin.test.BeforeTest import kotlin.test.Test @@ -126,6 +130,36 @@ class ProtocolTest { inFlight.await() } + @Test + fun `should propagate CancellationException from notification handler without calling onError`() = runTest { + protocol.connect(transport) + + protocol.fallbackNotificationHandler = { + throw CancellationException("test cancellation") + } + + shouldThrow { + transport.deliver(JSONRPCNotification(method = "test/notification")) + } + + protocol.errors shouldHaveSize 0 + } + + @Test + fun `should report non-cancellation exception from notification handler via onError`() = runTest { + protocol.connect(transport) + + protocol.fallbackNotificationHandler = { + throw IllegalStateException("handler failed") + } + + // Non-CE exceptions are caught and reported, not propagated + transport.deliver(JSONRPCNotification(method = "test/notification")) + + protocol.errors shouldHaveSize 1 + protocol.errors[0].message shouldBe "handler failed" + } + @Test fun `should create params object when request params are null`() = runTest { protocol.connect(transport) @@ -154,6 +188,12 @@ class ProtocolTest { } private class TestProtocol : Protocol(null) { + val errors = mutableListOf() + + override fun onError(error: Throwable) { + errors.add(error) + } + override fun assertCapabilityForMethod(method: Method) { // noop } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt index f8c658129..fcf1a466c 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt @@ -84,6 +84,8 @@ public class SseServerTransport(private val endpoint: String, private val sessio } call.receiveText() + } catch (e: CancellationException) { + throw e } catch (e: Exception) { call.respondText("Invalid message: ${e.message}", status = HttpStatusCode.BadRequest) _onError.invoke(e) @@ -92,6 +94,8 @@ public class SseServerTransport(private val endpoint: String, private val sessio try { handleMessage(body) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { call.respondText("Error handling message $body: ${e.message}", status = HttpStatusCode.BadRequest) return @@ -108,6 +112,8 @@ public class SseServerTransport(private val endpoint: String, private val sessio try { val parsedMessage = McpJson.decodeFromString(message) _onMessage.invoke(parsedMessage) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { _onError.invoke(e) throw e diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 9c105f94f..497005a48 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -28,15 +28,18 @@ import io.modelcontextprotocol.kotlin.sdk.types.RPCError import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.REQUEST_TIMEOUT import io.modelcontextprotocol.kotlin.sdk.types.RequestId import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.job import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.decodeFromJsonElement import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.uuid.ExperimentalUuidApi @@ -283,17 +286,19 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } override suspend fun close() { - streamMutex.withLock { - streamsMapping.values.forEach { - try { - it.session?.close() - } catch (_: Exception) { + withContext(NonCancellable) { + streamMutex.withLock { + streamsMapping.values.forEach { + try { + it.session?.close() + } catch (_: Exception) { + } } + streamsMapping.clear() + requestToStreamMapping.clear() + requestToResponseMapping.clear() + invokeOnCloseCallback() } - streamsMapping.clear() - requestToStreamMapping.clear() - requestToResponseMapping.clear() - invokeOnCloseCallback() } } @@ -420,6 +425,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat call.coroutineContext.job.invokeOnCompletion { streamsMapping.remove(streamId) } messages.forEach { message -> _onMessage(message) } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { call.reject( HttpStatusCode.BadRequest, @@ -498,12 +505,14 @@ public class StreamableHttpServerTransport(private val configuration: Configurat val streamId = requestToStreamMapping[requestId] ?: return val sessionContext = streamsMapping[streamId] ?: return - try { - sessionContext.session?.close() - } catch (e: Exception) { - _onError(e) - } finally { - streamsMapping.remove(streamId) + withContext(NonCancellable) { + try { + sessionContext.session?.close() + } catch (e: Exception) { + _onError(e) + } finally { + streamsMapping.remove(streamId) + } } } @@ -554,6 +563,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat id = eventId, data = McpJson.encodeToString(message), ) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { _onError(IllegalStateException("Failed to replay event: ${e.message}", e)) } @@ -565,6 +576,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat streamsMapping.remove(streamId) throwable?.let { _onError(it) } } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { _onError(e) } @@ -658,6 +671,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat private suspend fun flushSse(session: ServerSSESession?) { try { session?.send(data = "") + } catch (e: CancellationException) { + throw e } catch (e: Exception) { _onError(e) } @@ -709,6 +724,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat val eventId = configuration.eventStore?.storeEvent(streamId, message) try { session?.send(event = "message", id = eventId, data = McpJson.encodeToString(message)) + } catch (e: CancellationException) { + streamsMapping.remove(streamId) + throw e } catch (_: Exception) { streamsMapping.remove(streamId) } @@ -733,6 +751,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat retry = configuration.retryInterval?.inWholeMilliseconds, data = "", ) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { _onError(e) } From 8e87cd86f494f4b92a128ae4bf2f1f19565537a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:41:35 +0200 Subject: [PATCH 097/187] chore(deps): bump the kotlin group across 1 directory with 2 updates (#704) Bumps the kotlin group with 2 updates in the / directory: [org.jetbrains.kotlin:kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) and [org.jetbrains.kotlin:kotlin-serialization](https://github.com/JetBrains/kotlin). Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 2.3.10 to 2.3.20
Release notes

Sourced from org.jetbrains.kotlin:kotlin-gradle-plugin's releases.

Kotlin 2.3.20

Changelog

Analysis API. FIR

New Features

  • KT-78090 Implement stubs support for new conditional returns and holdsIn contracts

Fixes

  • KT-82948 'FirRegularClass' expected as a containing declaration, got 'FirTypeAliasImpl'
  • KT-83467 Package-level JSpecify annotations are ignored when coming from jars or libraries
  • KT-82057 K2. Cannot infer type parameter 'R' in Ktor routing post() function with explicit response type
  • KT-82846 AA: unresolved KtExpression.expressionType for the reference to the parameter with default value
  • KT-80485 False positive UNRESOLVED_REFERENCE on nested interface from super-super class in the super type position inside an anonymous object
  • KT-82772 Flaky false positive deprecation warning on PersistentMap.put in Kotlin repo in IDE mode
  • KT-76487 StdLibSourcesLazyDeclarationResolveTestGenerated.testWrappedInt is unstable
  • KT-82618 Various tests are failing with NPE in kt-master after updating the compiler on 19.11.25
  • KT-82076 Error querying members of JavaClass created for SymbolLightClassForAnnotationClass during library analysis
  • KT-71596 Include Js/Wasi checkers in AbstractLLFirDiagnosticsCollector
  • KT-82085 No OUTER_CLASS_ARGUMENTS_REQUIRED on type parameter bound in IDE
  • KT-81873 Provide a way of including traces in phase JFR events
  • KT-71929 Consider leaving the non-post-compute version at EnhancementSymbolsCache.enhancedFunctions

Analysis API. Infrastructure

  • KT-83173 Analysis API Tests: Library names with RC versions aren't sanitised
  • KT-65140 LL FIR: Implement AbstractFirPsiJsDiagnosticTest for LL FIR
  • KT-82212 [Analysis API, LL FIR] Implement AbstractDiagnosticsFirWasmTest and AbstractDiagnosticsFirWasmWasiTest for LL FIR

Analysis API. Light Classes

  • KT-82227 Value classes should expose regular static methods

Analysis API. PSI

  • KT-81710 'KtTypeReference.getTypeText' does not account for 'suspend' modifier on suspend lambdas
  • KT-82258 Prepare PSI for migration from context receivers to context parameters
  • KT-81074 KDoc: List rendering is broken

Analysis API. Providers and Caches

  • KT-82449 K2 IDE Analysis Freezes During Gradle Sync (Recursive Module Dependency Computation in KotlinModuleDependentsProviderBase.computeTransitiveDependents)
  • KT-82629 'collectDiagnostics' returns stale syntax error after editor fix
  • KT-74907 Analysis API: Apply platform-based library module content restrictions consistently

Analysis API. Standalone

  • KT-81107 AA: KtSourceModuleBuilder.sourceRoots doesn't works with symbolic links

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin:kotlin-gradle-plugin's changelog.

2.3.20

Analysis API. FIR

New Features

  • KT-78090 Implement stubs support for new conditional returns and holdsIn contracts

Fixes

  • KT-82948 'FirRegularClass' expected as a containing declaration, got 'FirTypeAliasImpl'
  • KT-83467 Package-level JSpecify annotations are ignored when coming from jars or libraries
  • KT-82057 K2. Cannot infer type parameter 'R' in Ktor routing post() function with explicit response type
  • KT-82846 AA: unresolved KtExpression.expressionType for the reference to the parameter with default value
  • KT-80485 False positive UNRESOLVED_REFERENCE on nested interface from super-super class in the super type position inside an anonymous object
  • KT-82772 Flaky false positive deprecation warning on PersistentMap.put in Kotlin repo in IDE mode
  • KT-76487 StdLibSourcesLazyDeclarationResolveTestGenerated.testWrappedInt is unstable
  • KT-82618 Various tests are failing with NPE in kt-master after updating the compiler on 19.11.25
  • KT-82076 Error querying members of JavaClass created for SymbolLightClassForAnnotationClass during library analysis
  • KT-71596 Include Js/Wasi checkers in AbstractLLFirDiagnosticsCollector
  • KT-82085 No OUTER_CLASS_ARGUMENTS_REQUIRED on type parameter bound in IDE
  • KT-81873 Provide a way of including traces in phase JFR events
  • KT-71929 Consider leaving the non-post-compute version at EnhancementSymbolsCache.enhancedFunctions

Analysis API. Infrastructure

  • KT-83173 Analysis API Tests: Library names with RC versions aren't sanitised
  • KT-65140 LL FIR: Implement AbstractFirPsiJsDiagnosticTest for LL FIR
  • KT-82212 [Analysis API, LL FIR] Implement AbstractDiagnosticsFirWasmTest and AbstractDiagnosticsFirWasmWasiTest for LL FIR

Analysis API. Light Classes

  • KT-82227 Value classes should expose regular static methods

Analysis API. PSI

  • KT-81710 'KtTypeReference.getTypeText' does not account for 'suspend' modifier on suspend lambdas
  • KT-82258 Prepare PSI for migration from context receivers to context parameters
  • KT-81074 KDoc: List rendering is broken

Analysis API. Providers and Caches

  • KT-82449 K2 IDE Analysis Freezes During Gradle Sync (Recursive Module Dependency Computation in KotlinModuleDependentsProviderBase.computeTransitiveDependents)
  • KT-82629 'collectDiagnostics' returns stale syntax error after editor fix
  • KT-74907 Analysis API: Apply platform-based library module content restrictions consistently

Analysis API. Standalone

  • KT-81107 AA: KtSourceModuleBuilder.sourceRoots doesn't works with symbolic links

... (truncated)

Commits
  • d57eb4a Add ChangeLog for 2.3.20-RC3
  • 41886bb [Gradle] Update KGP npm tooling dependency versions
  • 210a9bd [Native] Do not mark iosX64 target as deprecate in DSL
  • 7ed446e Add ChangeLog for 2.3.20-RC2
  • 520eade [K/JVM] Fix incorrect optimization of local delegation
  • 7ef8bcc Reflection: add LazyKProperty to fix recent performance regression
  • 9fa7cf2 [Gradle] Link NoActiveThreadsAfterCompilerInvocationIT with KT-84566
  • d410596 [Gradle] Shutdown coroutines dispatcher threads after compiler invocation
  • 348430b [BTA] Fix compatibility between API 2.3.20 and KC 2.3.10
  • 4058f3c [CMP] HiddenFromObjC remove outdated offset check
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlin:kotlin-serialization` from 2.3.10 to 2.3.20
Release notes

Sourced from org.jetbrains.kotlin:kotlin-serialization's releases.

Kotlin 2.3.20

Changelog

Analysis API. FIR

New Features

  • KT-78090 Implement stubs support for new conditional returns and holdsIn contracts

Fixes

  • KT-82948 'FirRegularClass' expected as a containing declaration, got 'FirTypeAliasImpl'
  • KT-83467 Package-level JSpecify annotations are ignored when coming from jars or libraries
  • KT-82057 K2. Cannot infer type parameter 'R' in Ktor routing post() function with explicit response type
  • KT-82846 AA: unresolved KtExpression.expressionType for the reference to the parameter with default value
  • KT-80485 False positive UNRESOLVED_REFERENCE on nested interface from super-super class in the super type position inside an anonymous object
  • KT-82772 Flaky false positive deprecation warning on PersistentMap.put in Kotlin repo in IDE mode
  • KT-76487 StdLibSourcesLazyDeclarationResolveTestGenerated.testWrappedInt is unstable
  • KT-82618 Various tests are failing with NPE in kt-master after updating the compiler on 19.11.25
  • KT-82076 Error querying members of JavaClass created for SymbolLightClassForAnnotationClass during library analysis
  • KT-71596 Include Js/Wasi checkers in AbstractLLFirDiagnosticsCollector
  • KT-82085 No OUTER_CLASS_ARGUMENTS_REQUIRED on type parameter bound in IDE
  • KT-81873 Provide a way of including traces in phase JFR events
  • KT-71929 Consider leaving the non-post-compute version at EnhancementSymbolsCache.enhancedFunctions

Analysis API. Infrastructure

  • KT-83173 Analysis API Tests: Library names with RC versions aren't sanitised
  • KT-65140 LL FIR: Implement AbstractFirPsiJsDiagnosticTest for LL FIR
  • KT-82212 [Analysis API, LL FIR] Implement AbstractDiagnosticsFirWasmTest and AbstractDiagnosticsFirWasmWasiTest for LL FIR

Analysis API. Light Classes

  • KT-82227 Value classes should expose regular static methods

Analysis API. PSI

  • KT-81710 'KtTypeReference.getTypeText' does not account for 'suspend' modifier on suspend lambdas
  • KT-82258 Prepare PSI for migration from context receivers to context parameters
  • KT-81074 KDoc: List rendering is broken

Analysis API. Providers and Caches

  • KT-82449 K2 IDE Analysis Freezes During Gradle Sync (Recursive Module Dependency Computation in KotlinModuleDependentsProviderBase.computeTransitiveDependents)
  • KT-82629 'collectDiagnostics' returns stale syntax error after editor fix
  • KT-74907 Analysis API: Apply platform-based library module content restrictions consistently

Analysis API. Standalone

  • KT-81107 AA: KtSourceModuleBuilder.sourceRoots doesn't works with symbolic links

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin:kotlin-serialization's changelog.

2.3.20

Analysis API. FIR

New Features

  • KT-78090 Implement stubs support for new conditional returns and holdsIn contracts

Fixes

  • KT-82948 'FirRegularClass' expected as a containing declaration, got 'FirTypeAliasImpl'
  • KT-83467 Package-level JSpecify annotations are ignored when coming from jars or libraries
  • KT-82057 K2. Cannot infer type parameter 'R' in Ktor routing post() function with explicit response type
  • KT-82846 AA: unresolved KtExpression.expressionType for the reference to the parameter with default value
  • KT-80485 False positive UNRESOLVED_REFERENCE on nested interface from super-super class in the super type position inside an anonymous object
  • KT-82772 Flaky false positive deprecation warning on PersistentMap.put in Kotlin repo in IDE mode
  • KT-76487 StdLibSourcesLazyDeclarationResolveTestGenerated.testWrappedInt is unstable
  • KT-82618 Various tests are failing with NPE in kt-master after updating the compiler on 19.11.25
  • KT-82076 Error querying members of JavaClass created for SymbolLightClassForAnnotationClass during library analysis
  • KT-71596 Include Js/Wasi checkers in AbstractLLFirDiagnosticsCollector
  • KT-82085 No OUTER_CLASS_ARGUMENTS_REQUIRED on type parameter bound in IDE
  • KT-81873 Provide a way of including traces in phase JFR events
  • KT-71929 Consider leaving the non-post-compute version at EnhancementSymbolsCache.enhancedFunctions

Analysis API. Infrastructure

  • KT-83173 Analysis API Tests: Library names with RC versions aren't sanitised
  • KT-65140 LL FIR: Implement AbstractFirPsiJsDiagnosticTest for LL FIR
  • KT-82212 [Analysis API, LL FIR] Implement AbstractDiagnosticsFirWasmTest and AbstractDiagnosticsFirWasmWasiTest for LL FIR

Analysis API. Light Classes

  • KT-82227 Value classes should expose regular static methods

Analysis API. PSI

  • KT-81710 'KtTypeReference.getTypeText' does not account for 'suspend' modifier on suspend lambdas
  • KT-82258 Prepare PSI for migration from context receivers to context parameters
  • KT-81074 KDoc: List rendering is broken

Analysis API. Providers and Caches

  • KT-82449 K2 IDE Analysis Freezes During Gradle Sync (Recursive Module Dependency Computation in KotlinModuleDependentsProviderBase.computeTransitiveDependents)
  • KT-82629 'collectDiagnostics' returns stale syntax error after editor fix
  • KT-74907 Analysis API: Apply platform-based library module content restrictions consistently

Analysis API. Standalone

  • KT-81107 AA: KtSourceModuleBuilder.sourceRoots doesn't works with symbolic links

... (truncated)

Commits
  • d57eb4a Add ChangeLog for 2.3.20-RC3
  • 41886bb [Gradle] Update KGP npm tooling dependency versions
  • 210a9bd [Native] Do not mark iosX64 target as deprecate in DSL
  • 7ed446e Add ChangeLog for 2.3.20-RC2
  • 520eade [K/JVM] Fix incorrect optimization of local delegation
  • 7ef8bcc Reflection: add LazyKProperty to fix recent performance regression
  • 9fa7cf2 [Gradle] Link NoActiveThreadsAfterCompilerInvocationIT with KT-84566
  • d410596 [Gradle] Shutdown coroutines dispatcher threads after compiler invocation
  • 348430b [BTA] Fix compatibility between API 2.3.20 and KC 2.3.10
  • 4058f3c [CMP] HiddenFromObjC remove outdated offset check
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlin:kotlin-serialization` from 2.3.10 to 2.3.20
Release notes

Sourced from org.jetbrains.kotlin:kotlin-serialization's releases.

Kotlin 2.3.20

Changelog

Analysis API. FIR

New Features

  • KT-78090 Implement stubs support for new conditional returns and holdsIn contracts

Fixes

  • KT-82948 'FirRegularClass' expected as a containing declaration, got 'FirTypeAliasImpl'
  • KT-83467 Package-level JSpecify annotations are ignored when coming from jars or libraries
  • KT-82057 K2. Cannot infer type parameter 'R' in Ktor routing post() function with explicit response type
  • KT-82846 AA: unresolved KtExpression.expressionType for the reference to the parameter with default value
  • KT-80485 False positive UNRESOLVED_REFERENCE on nested interface from super-super class in the super type position inside an anonymous object
  • KT-82772 Flaky false positive deprecation warning on PersistentMap.put in Kotlin repo in IDE mode
  • KT-76487 StdLibSourcesLazyDeclarationResolveTestGenerated.testWrappedInt is unstable
  • KT-82618 Various tests are failing with NPE in kt-master after updating the compiler on 19.11.25
  • KT-82076 Error querying members of JavaClass created for SymbolLightClassForAnnotationClass during library analysis
  • KT-71596 Include Js/Wasi checkers in AbstractLLFirDiagnosticsCollector
  • KT-82085 No OUTER_CLASS_ARGUMENTS_REQUIRED on type parameter bound in IDE
  • KT-81873 Provide a way of including traces in phase JFR events
  • KT-71929 Consider leaving the non-post-compute version at EnhancementSymbolsCache.enhancedFunctions

Analysis API. Infrastructure

  • KT-83173 Analysis API Tests: Library names with RC versions aren't sanitised
  • KT-65140 LL FIR: Implement AbstractFirPsiJsDiagnosticTest for LL FIR
  • KT-82212 [Analysis API, LL FIR] Implement AbstractDiagnosticsFirWasmTest and AbstractDiagnosticsFirWasmWasiTest for LL FIR

Analysis API. Light Classes

  • KT-82227 Value classes should expose regular static methods

Analysis API. PSI

  • KT-81710 'KtTypeReference.getTypeText' does not account for 'suspend' modifier on suspend lambdas
  • KT-82258 Prepare PSI for migration from context receivers to context parameters
  • KT-81074 KDoc: List rendering is broken

Analysis API. Providers and Caches

  • KT-82449 K2 IDE Analysis Freezes During Gradle Sync (Recursive Module Dependency Computation in KotlinModuleDependentsProviderBase.computeTransitiveDependents)
  • KT-82629 'collectDiagnostics' returns stale syntax error after editor fix
  • KT-74907 Analysis API: Apply platform-based library module content restrictions consistently

Analysis API. Standalone

  • KT-81107 AA: KtSourceModuleBuilder.sourceRoots doesn't works with symbolic links

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin:kotlin-serialization's changelog.

2.3.20

Analysis API. FIR

New Features

  • KT-78090 Implement stubs support for new conditional returns and holdsIn contracts

Fixes

  • KT-82948 'FirRegularClass' expected as a containing declaration, got 'FirTypeAliasImpl'
  • KT-83467 Package-level JSpecify annotations are ignored when coming from jars or libraries
  • KT-82057 K2. Cannot infer type parameter 'R' in Ktor routing post() function with explicit response type
  • KT-82846 AA: unresolved KtExpression.expressionType for the reference to the parameter with default value
  • KT-80485 False positive UNRESOLVED_REFERENCE on nested interface from super-super class in the super type position inside an anonymous object
  • KT-82772 Flaky false positive deprecation warning on PersistentMap.put in Kotlin repo in IDE mode
  • KT-76487 StdLibSourcesLazyDeclarationResolveTestGenerated.testWrappedInt is unstable
  • KT-82618 Various tests are failing with NPE in kt-master after updating the compiler on 19.11.25
  • KT-82076 Error querying members of JavaClass created for SymbolLightClassForAnnotationClass during library analysis
  • KT-71596 Include Js/Wasi checkers in AbstractLLFirDiagnosticsCollector
  • KT-82085 No OUTER_CLASS_ARGUMENTS_REQUIRED on type parameter bound in IDE
  • KT-81873 Provide a way of including traces in phase JFR events
  • KT-71929 Consider leaving the non-post-compute version at EnhancementSymbolsCache.enhancedFunctions

Analysis API. Infrastructure

  • KT-83173 Analysis API Tests: Library names with RC versions aren't sanitised
  • KT-65140 LL FIR: Implement AbstractFirPsiJsDiagnosticTest for LL FIR
  • KT-82212 [Analysis API, LL FIR] Implement AbstractDiagnosticsFirWasmTest and AbstractDiagnosticsFirWasmWasiTest for LL FIR

Analysis API. Light Classes

  • KT-82227 Value classes should expose regular static methods

Analysis API. PSI

  • KT-81710 'KtTypeReference.getTypeText' does not account for 'suspend' modifier on suspend lambdas
  • KT-82258 Prepare PSI for migration from context receivers to context parameters
  • KT-81074 KDoc: List rendering is broken

Analysis API. Providers and Caches

  • KT-82449 K2 IDE Analysis Freezes During Gradle Sync (Recursive Module Dependency Computation in KotlinModuleDependentsProviderBase.computeTransitiveDependents)
  • KT-82629 'collectDiagnostics' returns stale syntax error after editor fix
  • KT-74907 Analysis API: Apply platform-based library module content restrictions consistently

Analysis API. Standalone

  • KT-81107 AA: KtSourceModuleBuilder.sourceRoots doesn't works with symbolic links

... (truncated)

Commits
  • d57eb4a Add ChangeLog for 2.3.20-RC3
  • 41886bb [Gradle] Update KGP npm tooling dependency versions
  • 210a9bd [Native] Do not mark iosX64 target as deprecate in DSL
  • 7ed446e Add ChangeLog for 2.3.20-RC2
  • 520eade [K/JVM] Fix incorrect optimization of local delegation
  • 7ef8bcc Reflection: add LazyKProperty to fix recent performance regression
  • 9fa7cf2 [Gradle] Link NoActiveThreadsAfterCompilerInvocationIT with KT-84566
  • d410596 [Gradle] Shutdown coroutines dispatcher threads after compiler invocation
  • 348430b [BTA] Fix compatibility between API 2.3.20 and KC 2.3.10
  • 4058f3c [CMP] HiddenFromObjC remove outdated offset check
  • Additional commits viewable in compare view

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 0fb313501..ec12351d0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ atomicfu = "0.32.1" binaryCompatibilityValidatorPlugin = "0.18.1" detekt = "2.0.0-alpha.2" dokka = "2.2.0" -kotlin = "2.3.10" +kotlin = "2.3.20" kover = "0.9.8" ktlint = "14.2.0" knit = "0.5.1" From c6e6f7ca56028b06518c18a84be267d0a00e7fb8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:17:01 +0200 Subject: [PATCH 098/187] chore(deps): bump org.jetbrains.kotlinx:kotlinx-serialization-json from 1.10.0 to 1.11.0 in the kotlinx group across 1 directory (#687) Bumps the kotlinx group with 1 update in the / directory: [org.jetbrains.kotlinx:kotlinx-serialization-json](https://github.com/Kotlin/kotlinx.serialization). Updates `org.jetbrains.kotlinx:kotlinx-serialization-json` from 1.10.0 to 1.11.0
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-serialization-json's releases.

1.11.0

This release is based on Kotlin 2.3.20 and provides a new Json exceptions API and some bugfixes and improvements.

Expose Json exceptions structure

To make working with exceptions easier and providing proper error codes in e.g., REST APIs, classes JsonException, JsonDecodingException, and JsonEncodingException are now public. They have relevant public properties, such as shortMessage, path, offset, and others. This API is currently experimental, and we're going to improve it further in the subsequent releases. See the linked issues for the details: #1930, #1877.

Ability to hide user input from exception messages for security/privacy reasons.

Historically, exception messages in kotlinx.serialization often included the input Json itself for debuggability reason. Such behavior may pose additional challenges for logging, analytics, and other systems, since a system is not always allowed to store user data due to privacy/security reasons, which imposes additional sanitation logic. To address this issue, a new property exceptionsWithDebugInfo is added to JsonConfiguration. Disable it to hide user input from exception messages. IMPORTANT: This behavior will be enabled by default when this property becomes stable. See #2590 for more details.

Bugfixes and improvements

  • CBOR: Relax value range check when decoding numbers (#3167)
  • Use a specialized writeDecimalLong method for IO stream integrations in Json (#3152)
Changelog

Sourced from org.jetbrains.kotlinx:kotlinx-serialization-json's changelog.

1.11.0 / 2026-04-10

This release is based on Kotlin 2.3.20 and provides new Json exceptions API and some bugfixes and improvements.

Expose Json exceptions structure

To make working with exceptions easier and providing proper error codes in e.g., REST APIs, classes JsonException, JsonDecodingException, and JsonEncodingException are now public. They have relevant public properties, such as shortMessage, path, offset, and others. This API is currently experimental, and we're going to improve it further in the subsequent releases. See the linked issues for the details: #1930, #1877.

Ability to hide user input from exception messages for security/privacy reasons.

Historically, exception messages in kotlinx.serialization often included the input Json itself for debuggability reason. Such behavior may pose additional challenges for logging, analytics, and other systems, since a system is not always allowed to store user data due to privacy/security reasons, which imposes additional sanitation logic. To address this issue, a new property exceptionsWithDebugInfo is added to JsonConfiguration. Disable it to hide user input from exception messages. IMPORTANT: This behavior will be enabled by default when this property becomes stable. See #2590 for more details.

Bugfixes and improvements

  • CBOR: Relax value range check when decoding numbers (#3167)
  • Use a specialized writeDecimalLong method for IO stream integrations in Json (#3152)
Commits
  • 6956af2 Prepare 1.11 release
  • 390d84c Merge remote-tracking branch 'origin/master' into dev
  • 431fe2d Use local repo for publishing (#3171)
  • 05c12b6 Add usage attribute to "testRepositories" configuration
  • a4e1f08 Bump Kover version to 0.9.8 release (#3174)
  • 304e858 Expose Json exceptions structure (#3145)
  • 4a0338e Included G Play SDK verification file for core-jvm (#3169)
  • 421f64c CBOR: Relax value range check when decoding numbers (#3167)
  • 85a4f12 KT-84955: mark apple x64 tagets as deprecated error
  • bd38b0e Remove dead code
  • Additional commits viewable in compare view

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 ec12351d0..36478a99e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ ktor = "3.3.3" logging = "8.0.01" mockk = "1.14.9" mokksy = "0.8.1" -serialization = "1.10.0" +serialization = "1.11.0" slf4j = "2.0.17" logback = "1.5.32" junit = "5.14.3" From c1f0450d66fec4dd5168ba6ae9cf4facc8192d78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:25:44 +0200 Subject: [PATCH 099/187] chore(deps): bump hono from 4.12.12 to 4.12.14 in /integration-test/src/jvmTest/typescript (#707) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [hono](https://github.com/honojs/hono) from 4.12.12 to 4.12.14.
Release notes

Sourced from hono's releases.

v4.12.14

Security fixes

This release includes fixes for the following security issues:

Improper handling of JSX attribute names in hono/jsx SSR

Affects: hono/jsx. Fixes missing validation of JSX attribute names during server-side rendering, which could allow malformed attribute keys to corrupt the generated HTML output and inject unintended attributes or elements. GHSA-458j-xx4x-4375

Other changes

  • fix(aws-lambda): handle invalid header names in request processing (#4883) fa2c74fe

v4.12.13

What's Changed

New Contributors

Full Changelog: https://github.com/honojs/hono/compare/v4.12.12...v4.12.13

Commits
  • cf2d2b7 4.12.14
  • 66daa2e Merge commit from fork
  • fa2c74f fix(aws-lambda): handle invalid header names in request processing (#4883)
  • 3779927 4.12.13
  • faa6c46 feat(cache): add onCacheNotAvailable option (#4876)
  • f23e97b feat(trailing-slash): add skip option (#4862)
  • 1aa32fb fix(types): infer response type from last handler in app.on 9- and 10-handler...
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=hono&package-manager=npm_and_yarn&previous-version=4.12.12&new-version=4.12.14)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/modelcontextprotocol/kotlin-sdk/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index a246efaf5..f1f56f4e9 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -1197,9 +1197,9 @@ } }, "node_modules/hono": { - "version": "4.12.12", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", - "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "version": "4.12.14", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", + "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "license": "MIT", "engines": { "node": ">=16.9.0" From 74cda2e7420f77cf3273f61cdc821d69400d2d70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:46:41 +0200 Subject: [PATCH 100/187] chore(deps): bump com.anthropic:anthropic-java from 2.25.0 to 2.26.0 in /samples/kotlin-mcp-client in the other-dependencies group across 1 directory (#709) Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-client directory: [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java). Updates `com.anthropic:anthropic-java` from 2.25.0 to 2.26.0
Release notes

Sourced from com.anthropic:anthropic-java's releases.

v2.26.0

2.26.0 (2026-04-16)

Full Changelog: v2.25.0...v2.26.0

Features

  • api: add claude-opus-4-7, token budgets and user_profiles (a240f4f)

Chores

  • ci: remove release-doctor workflow (5d22f5b)
Changelog

Sourced from com.anthropic:anthropic-java's changelog.

2.26.0 (2026-04-16)

Full Changelog: v2.25.0...v2.26.0

Features

  • api: add claude-opus-4-7, token budgets and user_profiles (a240f4f)

Chores

  • ci: remove release-doctor workflow (5d22f5b)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.anthropic:anthropic-java&package-manager=gradle&previous-version=2.25.0&new-version=2.26.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 695b94dc0..5de32f0b1 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -anthropic = "2.25.0" +anthropic = "2.26.0" kotlin = "2.2.21" ktor = "3.3.3" mcp-kotlin = "0.11.1" From 1d21fa85af079dacdc69a521d4e87910c0ccedcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:27:27 +0000 Subject: [PATCH 101/187] chore(deps): bump actions/upload-pages-artifact from 4 to 5 in the all-actions group across 1 directory (#711) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-actions group with 1 update in the / directory: [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact). Updates `actions/upload-pages-artifact` from 4 to 5
Release notes

Sourced from actions/upload-pages-artifact's releases.

v5.0.0

Changelog

See details of all code changes since previous release.

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/apidocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/apidocs.yaml b/.github/workflows/apidocs.yaml index 76f4434a2..995f7d8bb 100644 --- a/.github/workflows/apidocs.yaml +++ b/.github/workflows/apidocs.yaml @@ -41,7 +41,7 @@ jobs: ./gradlew knit docs:classes docs:knitCheck dokkaGenerate - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: build/dokka/html From f6ad3322284c78b74ed6a24d7743aaac7b7d9550 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:55:25 +0000 Subject: [PATCH 102/187] chore(deps): bump typescript from 6.0.2 to 6.0.3 in /integration-test/src/jvmTest/typescript in the all-dependencies group across 1 directory (#712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-dependencies group with 1 update in the /integration-test/src/jvmTest/typescript directory: [typescript](https://github.com/microsoft/TypeScript). Updates `typescript` from 6.0.2 to 6.0.3
Release notes

Sourced from typescript's releases.

TypeScript 6.0.3

For release notes, check out the release announcement blog post.

Downloads are available on:

Commits
  • 050880c Bump version to 6.0.3 and LKG
  • eeae9dd 🤖 Pick PR #63401 (Also check package name validity in...) into release-6.0 (#...
  • ad1c695 🤖 Pick PR #63368 (Harden ATA package name filtering) into release-6.0 (#63372)
  • 0725fb4 🤖 Pick PR #63310 (Mark class property initializers as...) into release-6.0 (#...
  • See full diff in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 8 ++++---- integration-test/src/jvmTest/typescript/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index f1f56f4e9..34e6bf45e 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -18,7 +18,7 @@ "@types/node": "^25.6.0", "cors": "^2.8.6", "tsx": "^4.21.0", - "typescript": "^6.0.2" + "typescript": "^6.0.3" } }, "node_modules/@esbuild/aix-ppc64": { @@ -1737,9 +1737,9 @@ } }, "node_modules/typescript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", - "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index e38cca6e8..030c79ca1 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -19,6 +19,6 @@ "@types/node": "^25.6.0", "cors": "^2.8.6", "tsx": "^4.21.0", - "typescript": "^6.0.2" + "typescript": "^6.0.3" } } From bf8dc6d6b1b476364d779e1a177f905428bf74a2 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 23 Apr 2026 10:22:18 -0400 Subject: [PATCH 103/187] feat(core): add extensions field to ClientCapabilities and ServerCapabilities (#678) Add support for MCP extension negotiation during the initialize handshake, as defined in the MCP extensions specification. Clients and servers can now advertise supported extensions via a new `extensions: Map?` field on their respective capabilities objects. ## Motivation and Context Extensions are supported part of the [MCP spec ](https://modelcontextprotocol.io/extensions/overview) and are available in other language libraries e.g. [Golang ](https://github.com/modelcontextprotocol/go-sdk/blob/862d78a265cb1f1b9692ff49cfb8bfd109b6ebc9/mcp/protocol.go#L215). ## How Has This Been Tested? Existing and added unit tests ## Breaking Changes Adding a new field to existing ClientCapabilities and ServerCapabilities fields will break already compiled clients. They will need to recompile on upgrading. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context Co-authored-by: Pavel Gorgulov --- kotlin-sdk-core/api/kotlin-sdk-core.api | 21 ++-- .../kotlin/sdk/types/capabilities.dsl.kt | 26 +++++ .../kotlin/sdk/types/capabilities.kt | 8 ++ .../kotlin/sdk/types/CapabilitiesTest.kt | 97 +++++++++++++++++++ .../sdk/types/dsl/CapabilitiesDslTest.kt | 59 +++++++++++ .../kotlin/sdk/types/dsl/InitializeDslTest.kt | 14 +++ 6 files changed, 217 insertions(+), 8 deletions(-) diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index 679b68d27..b6fe94228 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -606,17 +606,19 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotificatio public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Companion; public fun ()V - public fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lkotlinx/serialization/json/JsonObject; public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; public final fun component3 ()Lkotlinx/serialization/json/JsonObject; public final fun component4 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public final fun component5 ()Ljava/util/Map; + public final fun copy (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; public fun equals (Ljava/lang/Object;)Z public final fun getElicitation ()Lkotlinx/serialization/json/JsonObject; public final fun getExperimental ()Lkotlinx/serialization/json/JsonObject; + public final fun getExtensions ()Ljava/util/Map; public final fun getRoots ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; public final fun getSampling ()Lkotlinx/serialization/json/JsonObject; public fun hashCode ()I @@ -676,6 +678,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilitiesBu public final fun elicitation (Lkotlinx/serialization/json/JsonObject;)V public final fun experimental (Lkotlin/jvm/functions/Function1;)V public final fun experimental (Lkotlinx/serialization/json/JsonObject;)V + public final fun extensions (Ljava/util/Map;)V public final fun roots (Ljava/lang/Boolean;)V public static synthetic fun roots$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilitiesBuilder;Ljava/lang/Boolean;ILjava/lang/Object;)V public final fun sampling (Lkotlin/jvm/functions/Function1;)V @@ -4245,19 +4248,21 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Sampling_dslKt { public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Companion; public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; public final fun component4 ()Lkotlinx/serialization/json/JsonObject; public final fun component5 ()Lkotlinx/serialization/json/JsonObject; public final fun component6 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public final fun component7 ()Ljava/util/Map; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; public fun equals (Ljava/lang/Object;)Z public final fun getCompletions ()Lkotlinx/serialization/json/JsonObject; public final fun getExperimental ()Lkotlinx/serialization/json/JsonObject; + public final fun getExtensions ()Ljava/util/Map; public final fun getLogging ()Lkotlinx/serialization/json/JsonObject; public final fun getPrompts ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; public final fun getResources ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt index 146eabfb2..d936381d9 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt @@ -39,6 +39,7 @@ public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { private var sampling: JsonObject? = null private var roots: ClientCapabilities.Roots? = null private var elicitation: JsonObject? = null + private var extensions: Map? = null private var experimental: JsonObject? = null /** @@ -132,6 +133,30 @@ public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { */ public fun elicitation(block: JsonObjectBuilder.() -> Unit): Unit = elicitation(buildJsonObject(block)) + /** + * Defines extensions that the client supports. + * + * Extension identifiers use the format `{vendor-prefix}/{extension-name}`, + * e.g., `"io.modelcontextprotocol/ui"`. Each value is an extension-specific + * settings object; an empty [JsonObject] indicates no settings. + * + * Example: + * ```kotlin + * capabilities { + * extensions(mapOf( + * "io.modelcontextprotocol/ui" to buildJsonObject { + * put("mimeTypes", JsonArray(listOf(JsonPrimitive("text/html")))) + * } + * )) + * } + * ``` + * + * @param value The map of extension identifiers to their settings + */ + public fun extensions(value: Map) { + this.extensions = value + } + /** * Defines experimental, non-standard capabilities that the client supports. * @@ -177,6 +202,7 @@ public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { sampling = sampling, roots = roots, elicitation = elicitation, + extensions = extensions, experimental = experimental, ) } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt index fae24c319..14248fae7 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt @@ -45,6 +45,9 @@ public data class Implementation( * @property elicitation Present if the client supports elicitation from the server. * @property experimental Experimental, non-standard capabilities that the client supports. * Keys are capability names, values are capability-specific configuration objects. + * @property extensions Optional extensions that the client supports. + * Keys are extension identifiers (e.g., `"io.modelcontextprotocol/ui"`), + * values are extension-specific settings objects. */ @Serializable public data class ClientCapabilities( @@ -52,6 +55,7 @@ public data class ClientCapabilities( public val roots: Roots? = null, public val elicitation: JsonObject? = null, public val experimental: JsonObject? = null, + public val extensions: Map? = null, ) { /** @@ -93,6 +97,9 @@ public data class ClientCapabilities( * @property completions Present if the server supports argument autocompletion suggestions. * Keys are capability names, values are capability-specific configuration objects. * @property experimental Experimental, non-standard capabilities that the server supports. + * @property extensions Optional extensions that the server supports. + * Keys are extension identifiers (e.g., `"io.modelcontextprotocol/ui"`), + * values are extension-specific settings objects. */ @Serializable public data class ServerCapabilities( @@ -102,6 +109,7 @@ public data class ServerCapabilities( val logging: JsonObject? = null, val completions: JsonObject? = null, val experimental: JsonObject? = null, + val extensions: Map? = null, ) { /** diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt index 33d565374..171c249dc 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt @@ -182,6 +182,53 @@ class CapabilitiesTest { ) } + @Test + fun `should serialize ClientCapabilities with extensions`() { + val extensions = mapOf( + "io.modelcontextprotocol/ui" to buildJsonObject { + put("mimeTypes", buildJsonObject { put("text/html", true) }) + }, + ) + val capabilities = ClientCapabilities( + extensions = extensions, + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "extensions": { + "io.modelcontextprotocol/ui": { + "mimeTypes": { + "text/html": true + } + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ClientCapabilities with empty extensions settings`() { + val capabilities = ClientCapabilities( + extensions = mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + ), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "extensions": { + "io.modelcontextprotocol/ui": {} + } + } + """.trimIndent(), + ) + } + @Test fun `should serialize ClientCapabilities with experimental`() { val experimental = buildJsonObject { @@ -215,11 +262,15 @@ class CapabilitiesTest { val experimental = buildJsonObject { put("feature1", buildJsonObject { put("enabled", true) }) } + val extensions = mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + ) val capabilities = ClientCapabilities( sampling = ClientCapabilities.sampling, roots = ClientCapabilities.Roots(listChanged = true), elicitation = ClientCapabilities.elicitation, experimental = experimental, + extensions = extensions, ) verifySerialization( capabilities, @@ -235,6 +286,9 @@ class CapabilitiesTest { "feature1": { "enabled": true } + }, + "extensions": { + "io.modelcontextprotocol/ui": {} } } """.trimIndent(), @@ -270,6 +324,7 @@ class CapabilitiesTest { assertNull(capabilities.roots) assertNull(capabilities.elicitation) assertNull(capabilities.experimental) + assertNull(capabilities.extensions) } @Test @@ -278,6 +333,11 @@ class CapabilitiesTest { sampling = ClientCapabilities.sampling, roots = ClientCapabilities.Roots(listChanged = false), elicitation = ClientCapabilities.elicitation, + extensions = mapOf( + "io.modelcontextprotocol/ui" to buildJsonObject { + put("mimeTypes", buildJsonObject { put("text/html", true) }) + }, + ), ) verifySerializationRoundTrip(original, McpJson) @@ -466,6 +526,32 @@ class CapabilitiesTest { ) } + @Test + fun `should serialize ServerCapabilities with extensions`() { + val capabilities = ServerCapabilities( + extensions = mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + "com.example/custom" to buildJsonObject { + put("setting", "value") + }, + ), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "extensions": { + "io.modelcontextprotocol/ui": {}, + "com.example/custom": { + "setting": "value" + } + } + } + """.trimIndent(), + ) + } + @Test fun `should serialize ServerCapabilities with experimental`() { val experimental = buildJsonObject { @@ -499,6 +585,9 @@ class CapabilitiesTest { val experimental = buildJsonObject { put("feature1", buildJsonObject { put("enabled", true) }) } + val extensions = mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + ) val capabilities = ServerCapabilities( tools = ServerCapabilities.Tools(listChanged = true), resources = ServerCapabilities.Resources( @@ -509,6 +598,7 @@ class CapabilitiesTest { logging = ServerCapabilities.Logging, completions = ServerCapabilities.Completions, experimental = experimental, + extensions = extensions, ) verifySerialization( capabilities, @@ -531,6 +621,9 @@ class CapabilitiesTest { "feature1": { "enabled": true } + }, + "extensions": { + "io.modelcontextprotocol/ui": {} } } """.trimIndent(), @@ -578,6 +671,7 @@ class CapabilitiesTest { assertNull(capabilities.logging) assertNull(capabilities.completions) assertNull(capabilities.experimental) + assertNull(capabilities.extensions) } @Test @@ -590,6 +684,9 @@ class CapabilitiesTest { ), prompts = ServerCapabilities.Prompts(listChanged = true), logging = ServerCapabilities.Logging, + extensions = mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + ), ) verifySerializationRoundTrip(original, McpJson) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt index e4a949fde..53dd5c79f 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt @@ -4,7 +4,9 @@ import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.buildInitializeRequest +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.double import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put @@ -31,6 +33,7 @@ class CapabilitiesDslTest { roots.shouldBeNull() elicitation.shouldBeNull() experimental.shouldBeNull() + extensions.shouldBeNull() } } @@ -56,6 +59,13 @@ class CapabilitiesDslTest { put("beta", false) put("maxConcurrency", 10) } + extensions( + mapOf( + "io.modelcontextprotocol/ui" to buildJsonObject { + put("mimeTypes", "text/html") + }, + ), + ) } info("Test", "1.0") } @@ -80,6 +90,11 @@ class CapabilitiesDslTest { get("beta")?.jsonPrimitive?.content shouldBe "false" get("maxConcurrency")?.jsonPrimitive?.content shouldBe "10" } + extensions shouldNotBeNull { + get("io.modelcontextprotocol/ui") shouldNotBeNull { + get("mimeTypes")?.jsonPrimitive?.content shouldBe "text/html" + } + } } } @@ -110,6 +125,50 @@ class CapabilitiesDslTest { requestNull.params.capabilities.roots?.listChanged.shouldBeNull() } + @Test + fun `capabilities should build with extensions containing empty settings`() { + val request = buildInitializeRequest { + protocolVersion = "2024-11-05" + capabilities { + extensions( + mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + "com.example/custom" to EmptyJsonObject, + ), + ) + } + info("Test", "1.0") + } + + request.params.capabilities.shouldNotBeNull { + extensions shouldNotBeNull { + size shouldBe 2 + get("io.modelcontextprotocol/ui") shouldBe EmptyJsonObject + get("com.example/custom") shouldBe EmptyJsonObject + } + } + } + + @Test + fun `capabilities should overwrite extensions when set multiple times`() { + val request = buildInitializeRequest { + protocolVersion = "2024-11-05" + capabilities { + extensions(mapOf("io.modelcontextprotocol/ui" to EmptyJsonObject)) + extensions(mapOf("com.example/custom" to EmptyJsonObject)) // Should overwrite + } + info("Test", "1.0") + } + + request.params.capabilities.shouldNotBeNull { + extensions shouldNotBeNull { + size shouldBe 1 + get("com.example/custom") shouldBe EmptyJsonObject + get("io.modelcontextprotocol/ui").shouldBeNull() + } + } + } + @Test fun `capabilities should overwrite when same field set multiple times`() { val request = buildInitializeRequest { diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt index cb3511571..aa53cce9d 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt @@ -5,6 +5,7 @@ import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.buildInitializeRequest import kotlinx.serialization.json.buildJsonObject @@ -30,6 +31,11 @@ class InitializeDslTest { experimental { put("custom", true) } + extensions( + mapOf( + "io.modelcontextprotocol/ui" to EmptyJsonObject, + ), + ) } info( name = "TestClient", @@ -45,6 +51,9 @@ class InitializeDslTest { roots?.listChanged shouldBe true elicitation?.get("mode")?.jsonPrimitive?.content shouldBe "interactive" experimental?.get("custom")?.jsonPrimitive?.content shouldBe "true" + extensions shouldNotBeNull { + get("io.modelcontextprotocol/ui") shouldBe EmptyJsonObject + } } request.params.clientInfo.shouldNotBeNull { name shouldBe "TestClient" @@ -74,6 +83,9 @@ class InitializeDslTest { val samplingObj = buildJsonObject { put("key", "value") } val elicitationObj = buildJsonObject { put("key", "value") } val experimentalObj = buildJsonObject { put("key", "value") } + val extensionsMap = mapOf( + "io.modelcontextprotocol/ui" to buildJsonObject { put("key", "value") }, + ) val request = buildInitializeRequest { protocolVersion = "1.0" @@ -81,6 +93,7 @@ class InitializeDslTest { sampling(samplingObj) elicitation(elicitationObj) experimental(experimentalObj) + extensions(extensionsMap) } info("Test", "1.0") } @@ -89,6 +102,7 @@ class InitializeDslTest { sampling shouldBe samplingObj elicitation shouldBe elicitationObj experimental shouldBe experimentalObj + extensions shouldBe extensionsMap } } From c946dfc3c261fd6334afe5d538e8327f2da00a0f Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 27 Apr 2026 15:07:03 +0200 Subject: [PATCH 104/187] sample: kotlinlang mcp server (#713) add new sample: kotlinlang mcp server with 2 tools - search - get_page ## How Has This Been Tested? inspector/unit ## Breaking Changes none ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- samples/README.md | 8 + samples/kotlinlang-mcp-server/Dockerfile | 14 + samples/kotlinlang-mcp-server/README.md | 105 ++++++++ .../kotlinlang-mcp-server/build.gradle.kts | 42 +++ .../kotlinlang-mcp-server/gradle.properties | 4 + .../gradle/libs.versions.toml | 31 +++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + samples/kotlinlang-mcp-server/gradlew | 248 ++++++++++++++++++ samples/kotlinlang-mcp-server/gradlew.bat | 93 +++++++ .../kotlinlang-mcp-server/settings.gradle.kts | 25 ++ .../kotlin/org/kotlinlang/mcp/Application.kt | 28 ++ .../org/kotlinlang/mcp/KotlinlangServer.kt | 118 +++++++++ .../kotlinlang/mcp/algolia/AlgoliaClient.kt | 38 +++ .../kotlinlang/mcp/algolia/AlgoliaModels.kt | 38 +++ .../org/kotlinlang/mcp/cache/TtlCache.kt | 71 +++++ .../org/kotlinlang/mcp/config/ServerConfig.kt | 18 ++ .../org/kotlinlang/mcp/content/PageFetcher.kt | 11 + .../org/kotlinlang/mcp/content/UrlMapper.kt | 20 ++ .../kotlinlang/mcp/tools/GetKotlinlangPage.kt | 55 ++++ .../kotlinlang/mcp/tools/SearchKotlinlang.kt | 62 +++++ .../src/main/resources/application.conf | 17 ++ .../src/main/resources/logback.xml | 15 ++ .../kotlinlang/mcp/KotlinlangServerTest.kt | 62 +++++ .../mcp/algolia/AlgoliaClientTest.kt | 166 ++++++++++++ .../mcp/algolia/AlgoliaModelsTest.kt | 126 +++++++++ .../org/kotlinlang/mcp/cache/TtlCacheTest.kt | 144 ++++++++++ .../kotlinlang/mcp/config/ServerConfigTest.kt | 35 +++ .../kotlinlang/mcp/content/PageFetcherTest.kt | 87 ++++++ .../kotlinlang/mcp/content/UrlMapperTest.kt | 91 +++++++ .../mcp/tools/GetKotlinlangPageTest.kt | 139 ++++++++++ .../mcp/tools/SearchKotlinlangTest.kt | 231 ++++++++++++++++ 32 files changed, 2149 insertions(+) create mode 100644 samples/kotlinlang-mcp-server/Dockerfile create mode 100644 samples/kotlinlang-mcp-server/README.md create mode 100644 samples/kotlinlang-mcp-server/build.gradle.kts create mode 100644 samples/kotlinlang-mcp-server/gradle.properties create mode 100644 samples/kotlinlang-mcp-server/gradle/libs.versions.toml create mode 100644 samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/kotlinlang-mcp-server/gradlew create mode 100644 samples/kotlinlang-mcp-server/gradlew.bat create mode 100644 samples/kotlinlang-mcp-server/settings.gradle.kts create mode 100644 samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/Application.kt create mode 100644 samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/KotlinlangServer.kt create mode 100644 samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClient.kt create mode 100644 samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModels.kt create mode 100644 samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/cache/TtlCache.kt create mode 100644 samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/config/ServerConfig.kt create mode 100644 samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/PageFetcher.kt create mode 100644 samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/UrlMapper.kt create mode 100644 samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPage.kt create mode 100644 samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlang.kt create mode 100644 samples/kotlinlang-mcp-server/src/main/resources/application.conf create mode 100644 samples/kotlinlang-mcp-server/src/main/resources/logback.xml create mode 100644 samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/KotlinlangServerTest.kt create mode 100644 samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClientTest.kt create mode 100644 samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModelsTest.kt create mode 100644 samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/cache/TtlCacheTest.kt create mode 100644 samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/config/ServerConfigTest.kt create mode 100644 samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/PageFetcherTest.kt create mode 100644 samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/UrlMapperTest.kt create mode 100644 samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPageTest.kt create mode 100644 samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlangTest.kt diff --git a/samples/README.md b/samples/README.md index 97893782d..f831a74b1 100644 --- a/samples/README.md +++ b/samples/README.md @@ -9,6 +9,7 @@ For background on the protocol itself, see the [MCP documentation](https://model | Sample | Type | Transport | MCP Features | |--------------------------------------------------------|-------------------|-----------------|------------------------------------| | [simple-streamable-server](./simple-streamable-server) | Server | Streamable HTTP | Tools, Resources, Prompts, Logging | +| [kotlinlang-mcp-server](./kotlinlang-mcp-server) | Server | Streamable HTTP | Tools | | [kotlin-mcp-server](./kotlin-mcp-server) | Server | STDIO, SSE | Tools, Resources, Prompts | | [weather-stdio-server](./weather-stdio-server) | Server | STDIO | Tools | | [kotlin-mcp-client](./kotlin-mcp-client) | Client | STDIO | Tool discovery & invocation | @@ -30,6 +31,13 @@ A minimal Streamable HTTP server with optional Bearer token authentication. Demo (`greet`, `multi-greet`), a prompt template, a resource, and server-to-client logging notifications. [Read more →](./simple-streamable-server) +### Kotlinlang MCP Server + +A Streamable HTTP server that exposes the official Kotlin documentation (kotlinlang.org) to LLM +clients — full-text search via Algolia and page retrieval in markdown. Demonstrates wrapping a real +external API in an MCP server with in-memory caching. +[Read more →](./kotlinlang-mcp-server) + ### Kotlin MCP Server A multi-transport server supporting STDIO, SSE (plain), and SSE (Ktor plugin). Useful for exploring diff --git a/samples/kotlinlang-mcp-server/Dockerfile b/samples/kotlinlang-mcp-server/Dockerfile new file mode 100644 index 000000000..ada7e567f --- /dev/null +++ b/samples/kotlinlang-mcp-server/Dockerfile @@ -0,0 +1,14 @@ +FROM gradle:jdk21-alpine AS build +WORKDIR /project +COPY gradle/ gradle/ +COPY gradlew settings.gradle.kts build.gradle.kts ./ +COPY src/ src/ +RUN ./gradlew shadowJar --no-daemon + +FROM eclipse-temurin:21-jre-alpine +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +WORKDIR /app +COPY --from=build /project/build/libs/*-all.jar app.jar +USER appuser +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/samples/kotlinlang-mcp-server/README.md b/samples/kotlinlang-mcp-server/README.md new file mode 100644 index 000000000..df0dd3e94 --- /dev/null +++ b/samples/kotlinlang-mcp-server/README.md @@ -0,0 +1,105 @@ +# Kotlinlang MCP Server + +A Streamable HTTP MCP server that exposes the official +[Kotlin documentation](https://kotlinlang.org/docs/) to LLM clients — full-text search via +[Algolia](https://www.algolia.com/) plus page retrieval in markdown through kotlinlang.org's +`_llms` endpoints. + +## Overview + +This sample demonstrates an MCP server that wraps a real external documentation source. It uses the +recommended Streamable HTTP transport, supports multiple concurrent client sessions, and caches +responses in memory to reduce upstream load. The server is intentionally read-only — it exposes two +tools, no prompts, no resources. + +## Prerequisites + +- JDK 21+ +- Algolia credentials for `kotlinlang.org` (exported as environment variables, see + [Configuration](#configuration)) + +## Build & Run + +```shell +export ALGOLIA_APP_ID=... +export ALGOLIA_API_KEY=... +export ALGOLIA_INDEX_NAME=... + +./gradlew run +``` + +The server starts on `http://localhost:8080/mcp` by default. + +Connect with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector): + +```shell +npx @modelcontextprotocol/inspector +``` + +In the Inspector UI, select **Streamable HTTP** transport and enter `http://localhost:8080/mcp`. + +## MCP Capabilities + +### Tools + +| Name | Description | +|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------| +| `search_kotlinlang` | Full-text search across Kotlin documentation. Returns up to 5 results filtered to `/docs/` pages. Responses cached for 10 minutes. | +| `get_kotlinlang_page` | Fetches a documentation page as markdown via kotlinlang.org's `_llms` endpoint. Accepts a path relative to `/docs/`. Cached for 1 hour. | + +Example paths accepted by `get_kotlinlang_page`: `coroutines-overview`, +`multiplatform/compose-multiplatform-and-jetpack-compose`. Leading/trailing slashes and an optional +`.html` suffix are normalized before resolution. + +## Configuration + +| Variable | Required | Default | Description | +|----------------------|----------|-----------|--------------------------| +| `ALGOLIA_APP_ID` | yes | — | Algolia application ID | +| `ALGOLIA_API_KEY` | yes | — | Algolia search API key | +| `ALGOLIA_INDEX_NAME` | yes | — | Algolia index name | +| `SERVER_PORT` | no | `8080` | HTTP server port | +| `SERVER_HOST` | no | `0.0.0.0` | HTTP server bind address | + +The server does not provide fallback values for `ALGOLIA_*`. Startup fails if any of those +variables are missing. + +## Docker + +Multi-stage build with Alpine-based images. Runs as a non-root user. + +```shell +docker build -t kotlinlang-mcp-server . + +docker run \ + -p 8080:8080 \ + -e ALGOLIA_APP_ID=... \ + -e ALGOLIA_API_KEY=... \ + -e ALGOLIA_INDEX_NAME=... \ + kotlinlang-mcp-server +``` + +## Connecting an MCP client + +Any MCP client that supports Streamable HTTP transport can connect: + +```json +{ + "mcpServers": { + "kotlinlang": { + "url": "http://localhost:8080/mcp" + } + } +} +``` + +## Limitations + +This sample is intended **for demonstration only**. Before production use, consider: + +- **In-memory cache only** — cached data is lost on server restart. +- **No authentication or authorization** — run behind a trusted proxy or restrict network access. +- **No rate limiting** on outgoing requests to Algolia and kotlinlang.org. +- **Permissive CORS** (`anyHost()`) — restrict to specific origins for any non-local deployment. +- **Streamable HTTP only** — no STDIO transport for local-only usage. +- **Tools only** — no MCP resources or prompts are exposed. diff --git a/samples/kotlinlang-mcp-server/build.gradle.kts b/samples/kotlinlang-mcp-server/build.gradle.kts new file mode 100644 index 000000000..f46f006ba --- /dev/null +++ b/samples/kotlinlang-mcp-server/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.shadow) + application +} + +group = "org.example" +version = "0.1.0" + +application { + mainClass.set("org.kotlinlang.mcp.ApplicationKt") +} + +dependencies { + implementation(platform(libs.ktor.bom)) + implementation(libs.mcp.kotlin.server) + implementation(libs.ktor.server.netty) + implementation(libs.ktor.server.sse) + implementation(libs.ktor.server.content.negotiation) + implementation(libs.ktor.server.cors) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.logback.classic) + + testImplementation(kotlin("test")) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.ktor.client.mock) + testImplementation(libs.mcp.kotlin.client) + testImplementation(libs.mcp.kotlin.testing) +} + +tasks.test { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(21) +} diff --git a/samples/kotlinlang-mcp-server/gradle.properties b/samples/kotlinlang-mcp-server/gradle.properties new file mode 100644 index 000000000..24a59763f --- /dev/null +++ b/samples/kotlinlang-mcp-server/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.configuration-cache=true +org.gradle.parallel=true +org.gradle.caching=true + diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml new file mode 100644 index 000000000..20dff78b5 --- /dev/null +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -0,0 +1,31 @@ +[versions] +kotlin = "2.3.20" +ktor = "3.3.3" +mcp-kotlin = "0.11.1" +serialization = "1.10.0" +coroutines = "1.10.2" +logback = "1.5.32" +shadow = "9.4.1" + +[libraries] +ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } +ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty" } +ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse" } +ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation" } +ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors" } +ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } +ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation" } +ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json" } +mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } +mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } +mcp-kotlin-testing = { group = "io.modelcontextprotocol", name = "kotlin-sdk-testing", version.ref = "mcp-kotlin" } +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" } +logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } +ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } diff --git a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.jar b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d997cfc60f4cff0e7451d19d49a82fa986695d07 GIT binary patch literal 48966 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

`Lem|V$A>eR0uN8Y&sdjtu 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

1bvi{DS7E@q1{$_8->K{_OWv2 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 literal 0 HcmV?d00001 diff --git a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..c61a118f7 --- /dev/null +++ b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/kotlinlang-mcp-server/gradlew b/samples/kotlinlang-mcp-server/gradlew new file mode 100755 index 000000000..739907dfd --- /dev/null +++ b/samples/kotlinlang-mcp-server/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 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/2d6327017519d23b96af35865dc997fcb544fb40/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 + + + +# 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" ) + + 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" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# 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/kotlinlang-mcp-server/gradlew.bat b/samples/kotlinlang-mcp-server/gradlew.bat new file mode 100644 index 000000000..c4bdd3ab8 --- /dev/null +++ b/samples/kotlinlang-mcp-server/gradlew.bat @@ -0,0 +1,93 @@ +@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 + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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/kotlinlang-mcp-server/settings.gradle.kts b/samples/kotlinlang-mcp-server/settings.gradle.kts new file mode 100644 index 000000000..bf867e05a --- /dev/null +++ b/samples/kotlinlang-mcp-server/settings.gradle.kts @@ -0,0 +1,25 @@ +rootProject.name = "kotlinlang-mcp-server" + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +dependencyResolutionManagement { + repositories { + mavenLocal() + mavenCentral() + } + + versionCatalogs { + create("libs") { + val mcpKotlinVersion = providers.gradleProperty( + "mcp.kotlin.overrideVersion", + ).orNull + if (mcpKotlinVersion != null) { + logger.lifecycle("Using the override version $mcpKotlinVersion of MCP Kotlin SDK") + version("mcp-kotlin", mcpKotlinVersion) + } + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/Application.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/Application.kt new file mode 100644 index 000000000..e50b1ad43 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/Application.kt @@ -0,0 +1,28 @@ +package org.kotlinlang.mcp + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.cors.routing.* +import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp +import org.kotlinlang.mcp.config.toServerConfig + +fun main(args: Array) = EngineMain.main(args) + +fun Application.module() { + val config = environment.config.toServerConfig() + val kotlinlangServer = KotlinlangServer(config) + + monitor.subscribe(ApplicationStopped) { + kotlinlangServer.close() + } + + install(CORS) { + anyHost() + allowHeader(HttpHeaders.ContentType) + } + + mcpStreamableHttp { + kotlinlangServer.server + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/KotlinlangServer.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/KotlinlangServer.kt new file mode 100644 index 000000000..081434295 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/KotlinlangServer.kt @@ -0,0 +1,118 @@ +package org.kotlinlang.mcp + +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.types.* +import kotlinx.serialization.json.* +import org.kotlinlang.mcp.algolia.AlgoliaClient +import org.kotlinlang.mcp.cache.TtlCache +import org.kotlinlang.mcp.config.ServerConfig +import org.kotlinlang.mcp.content.PageFetcher +import org.kotlinlang.mcp.tools.GetKotlinlangPage +import org.kotlinlang.mcp.tools.SearchKotlinlang +import org.slf4j.LoggerFactory +import java.io.Closeable +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes + +internal class KotlinlangServer(config: ServerConfig) : Closeable { + + private val logger = LoggerFactory.getLogger(KotlinlangServer::class.java) + + private val httpClient = HttpClient(CIO) { + install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } + install(HttpTimeout) { requestTimeoutMillis = 10_000 } + expectSuccess = true + } + + private val searchCache = TtlCache(10.minutes) + private val pageCache = TtlCache(1.hours) + + private val algoliaClient = AlgoliaClient(config, httpClient) + private val pageFetcher = PageFetcher(httpClient) + private val searchTool = SearchKotlinlang(algoliaClient, searchCache) + private val pageTool = GetKotlinlangPage(pageFetcher, pageCache) + + val server: Server = Server( + serverInfo = Implementation(name = "kotlinlang-mcp-server", version = "1.0.0"), + options = ServerOptions( + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(listChanged = false), + ), + ), + ) { + addTool( + name = "search_kotlinlang", + description = "Search across Kotlin documentation on kotlinlang.org to find relevant pages, " + + "code examples, API references, and guides. Use this tool when you need to answer questions " + + "about Kotlin language features, standard library, coroutines, multiplatform, tooling, or any " + + "other topic covered in the official Kotlin documentation. Returns up to 5 results with page " + + "titles, paths, and text snippets. To get the full content of a specific page, use the " + + "get_kotlinlang_page tool with the page path from the search results.", + inputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("query") { + put("type", "string") + put( + "description", + "A search query to find relevant Kotlin documentation pages " + + "(e.g. 'coroutines', 'sealed classes', 'multiplatform setup')", + ) + } + }, + required = listOf("query"), + ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = true), + ) { request -> + val query = request.arguments?.get("query")?.jsonPrimitive?.content + ?: return@addTool CallToolResult( + content = listOf(TextContent(text = "Missing required argument: query")), + isError = true, + ) + logger.info("search_kotlinlang query=\"{}\"", query) + searchTool.handle(query) + } + + addTool( + name = "get_kotlinlang_page", + description = "Retrieve the full content of a specific Kotlin documentation page from " + + "kotlinlang.org in md format. Use this tool when you already know the " + + "page path (e.g., from search results) and need the complete content of that page rather " + + "than just a snippet. If the page is not found, use search_kotlinlang to discover the " + + "correct path.", + inputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("path") { + put("type", "string") + put( + "description", + "Page path relative to /docs/, without extension. Use the page paths " + + "returned from search_kotlinlang results " + + "(e.g. 'coroutines-overview', " + + "'multiplatform/compose-multiplatform-and-jetpack-compose')", + ) + } + }, + required = listOf("path"), + ), + toolAnnotations = ToolAnnotations(readOnlyHint = true, openWorldHint = true), + ) { request -> + val path = request.arguments?.get("path")?.jsonPrimitive?.content + ?: return@addTool CallToolResult( + content = listOf(TextContent(text = "Missing required argument: path")), + isError = true, + ) + logger.info("get_kotlinlang_page path=\"{}\"", path) + pageTool.handle(path) + } + } + + override fun close() { + httpClient.close() + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClient.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClient.kt new file mode 100644 index 000000000..0d0db42f6 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClient.kt @@ -0,0 +1,38 @@ +package org.kotlinlang.mcp.algolia + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* +import org.kotlinlang.mcp.config.ServerConfig + +internal class AlgoliaClient( + private val config: ServerConfig, + private val httpClient: HttpClient, +) { + suspend fun search(query: String): AlgoliaSearchResponse { + val response = httpClient.post(buildSearchUrl()) { + header("x-algolia-application-id", config.algoliaAppId) + header("x-algolia-api-key", config.algoliaApiKey) + contentType(ContentType.Application.Json) + setBody( + AlgoliaSearchRequest( + query = query, + hitsPerPage = HITS_PER_PAGE, + attributesToRetrieve = ATTRIBUTES_TO_RETRIEVE, + attributesToSnippet = ATTRIBUTES_TO_SNIPPET, + ) + ) + } + return response.body() + } + + private fun buildSearchUrl(): String = + "https://${config.algoliaAppId}-dsn.algolia.net/1/indexes/${config.algoliaIndexName}/query" + + companion object { + private const val HITS_PER_PAGE = 15 + private val ATTRIBUTES_TO_RETRIEVE = listOf("objectID", "mainTitle", "url", "headings") + private val ATTRIBUTES_TO_SNIPPET = listOf("content:40") + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModels.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModels.kt new file mode 100644 index 000000000..88c465d75 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModels.kt @@ -0,0 +1,38 @@ +package org.kotlinlang.mcp.algolia + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class AlgoliaSearchRequest( + val query: String, + val hitsPerPage: Int, + val attributesToRetrieve: List, + val attributesToSnippet: List, +) + +@Serializable +internal data class AlgoliaSearchResponse( + val hits: List, +) + +@Serializable +internal data class AlgoliaHit( + val objectID: String, + val mainTitle: String? = null, + val url: String, + val headings: String? = null, + @SerialName("_snippetResult") + val snippetResult: SnippetResult? = null, +) + +@Serializable +internal data class SnippetResult( + val content: SnippetValue, +) + +@Serializable +internal data class SnippetValue( + val value: String, + val matchLevel: String, +) diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/cache/TtlCache.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/cache/TtlCache.kt new file mode 100644 index 000000000..7ba8de12e --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/cache/TtlCache.kt @@ -0,0 +1,71 @@ +package org.kotlinlang.mcp.cache + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import java.util.concurrent.CancellationException +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration +import kotlin.time.TimeMark +import kotlin.time.TimeSource + +internal class TtlCache( + private val ttl: Duration, + private val timeSource: TimeSource = TimeSource.Monotonic, +) { + private class Entry(val value: V, val createdAt: TimeMark) + + private val map = ConcurrentHashMap>() + private val inFlight = ConcurrentHashMap>() + + fun get(key: K): V? { + val entry = map[key] ?: return null + if (entry.createdAt.elapsedNow() >= ttl) { + map.remove(key, entry) + return null + } + return entry.value + } + + fun put(key: K, value: V) { + map[key] = Entry(value, timeSource.markNow()) + evictExpired() + } + + private fun evictExpired() { + map.entries.removeIf { it.value.createdAt.elapsedNow() >= ttl } + } + + suspend fun getOrPut(key: K, loader: suspend () -> V): V { + while (true) { + get(key)?.let { return it } + + val deferred = CompletableDeferred() + val existing = inFlight.putIfAbsent(key, deferred) + if (existing != null) { + try { + return existing.await() + } catch (_: CancellationException) { + currentCoroutineContext().ensureActive() + // The loader's scope was cancelled, not ours — retry + continue + } + } + + try { + val value = loader() + put(key, value) + deferred.complete(value) + return value + } catch (e: CancellationException) { + deferred.cancel() + throw e + } catch (e: Throwable) { + deferred.completeExceptionally(e) + throw e + } finally { + inFlight.remove(key, deferred) + } + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/config/ServerConfig.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/config/ServerConfig.kt new file mode 100644 index 000000000..0c61e13b7 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/config/ServerConfig.kt @@ -0,0 +1,18 @@ +package org.kotlinlang.mcp.config + +import io.ktor.server.config.* + +internal data class ServerConfig( + val algoliaAppId: String, + val algoliaApiKey: String, + val algoliaIndexName: String, +) { + override fun toString(): String = + "ServerConfig(algoliaAppId=$algoliaAppId, algoliaIndexName=$algoliaIndexName)" +} + +internal fun ApplicationConfig.toServerConfig(): ServerConfig = ServerConfig( + algoliaAppId = property("algolia.appId").getString(), + algoliaApiKey = property("algolia.apiKey").getString(), + algoliaIndexName = property("algolia.indexName").getString(), +) diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/PageFetcher.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/PageFetcher.kt new file mode 100644 index 000000000..a8495b266 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/PageFetcher.kt @@ -0,0 +1,11 @@ +package org.kotlinlang.mcp.content + +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* + +internal class PageFetcher(private val httpClient: HttpClient) { + suspend fun fetch(url: String): String { + return httpClient.get(url).bodyAsText() + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/UrlMapper.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/UrlMapper.kt new file mode 100644 index 000000000..ab0a8ff9a --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/content/UrlMapper.kt @@ -0,0 +1,20 @@ +package org.kotlinlang.mcp.content + +private const val BASE_URL = "https://kotlinlang.org/docs" + +internal fun normalizePath(path: String): String = path.trim().trim('/').removeSuffix(".html") + +internal fun mapPathToUrl(path: String): String { + val normalized = normalizePath(path) + require(normalized.isNotBlank()) { "Path must not be empty" } + require(".." !in normalized.split('/')) { "Path must not contain '..' segments" } + + val lastSlash = normalized.lastIndexOf('/') + return if (lastSlash == -1) { + "$BASE_URL/_llms/$normalized.txt" + } else { + val dir = normalized.substring(0, lastSlash) + val file = normalized.substring(lastSlash + 1) + "$BASE_URL/$dir/_llms/$file.txt" + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPage.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPage.kt new file mode 100644 index 000000000..d4a5097ac --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPage.kt @@ -0,0 +1,55 @@ +package org.kotlinlang.mcp.tools + +import io.ktor.client.plugins.* +import io.ktor.http.* +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import kotlinx.coroutines.CancellationException +import org.kotlinlang.mcp.cache.TtlCache +import org.kotlinlang.mcp.content.PageFetcher +import org.kotlinlang.mcp.content.mapPathToUrl +import org.kotlinlang.mcp.content.normalizePath +import org.slf4j.LoggerFactory + +internal class GetKotlinlangPage( + private val pageFetcher: PageFetcher, + private val cache: TtlCache, +) { + private val logger = LoggerFactory.getLogger(GetKotlinlangPage::class.java) + suspend fun handle(path: String): CallToolResult { + val normalizedPath = normalizePath(path) + return try { + val content = cache.getOrPut(normalizedPath) { + val url = mapPathToUrl(normalizedPath) + pageFetcher.fetch(url) + } + CallToolResult(content = listOf(TextContent(text = content))) + } catch (e: CancellationException) { + throw e + } catch (e: IllegalArgumentException) { + CallToolResult( + content = listOf(TextContent(text = "Invalid path: ${e.message}")), + isError = true, + ) + } catch (e: ClientRequestException) { + if (e.response.status != HttpStatusCode.NotFound) { + logger.error("Failed to fetch page path=\"{}\"", path, e) + } + val message = if (e.response.status == HttpStatusCode.NotFound) { + "Page not found: $path. Use search_kotlinlang to find the correct page path." + } else { + "Failed to fetch page: ${e.message}" + } + CallToolResult( + content = listOf(TextContent(text = message)), + isError = true, + ) + } catch (e: Exception) { + logger.error("Failed to fetch page path=\"{}\"", path, e) + CallToolResult( + content = listOf(TextContent(text = "Failed to fetch page: ${e.message ?: "unknown error"}")), + isError = true, + ) + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlang.kt b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlang.kt new file mode 100644 index 000000000..886f3f76f --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlang.kt @@ -0,0 +1,62 @@ +package org.kotlinlang.mcp.tools + +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import kotlinx.coroutines.CancellationException +import org.kotlinlang.mcp.algolia.AlgoliaClient +import org.kotlinlang.mcp.algolia.AlgoliaHit +import org.kotlinlang.mcp.cache.TtlCache +import org.slf4j.LoggerFactory + +internal class SearchKotlinlang( + private val algoliaClient: AlgoliaClient, + private val cache: TtlCache, +) { + private val logger = LoggerFactory.getLogger(SearchKotlinlang::class.java) + + companion object { + private const val MAX_RESULTS = 5 + private val HTML_TAG_REGEX = Regex("<[^>]+>") + } + + suspend fun handle(query: String): CallToolResult { + return try { + val text = cache.getOrPut(query) { + val response = algoliaClient.search(query) + val filtered = response.hits + .filter { it.url.startsWith("/docs/") } + .take(MAX_RESULTS) + if (filtered.isEmpty()) { + "No results found for: $query" + } else { + formatResults(filtered) + } + } + CallToolResult(content = listOf(TextContent(text = text))) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logger.error("Search failed for query=\"{}\"", query, e) + CallToolResult( + content = listOf(TextContent(text = "Search failed: ${e.message ?: "unknown error"}")), + isError = true, + ) + } + } + + private fun formatResults(hits: List): String = + hits.mapIndexed { index, hit -> + val title = hit.mainTitle ?: "Untitled" + val path = extractPath(hit.url) + val snippetLine = hit.snippetResult?.content?.value + ?.let { "\n ${stripHtml(it)}" } + ?: "" + "${index + 1}. $title [$path]$snippetLine" + }.joinToString("\n\n") + + private fun extractPath(url: String): String = + url.removePrefix("/docs/").removeSuffix(".html") + + private fun stripHtml(text: String): String = + text.replace(HTML_TAG_REGEX, "") +} diff --git a/samples/kotlinlang-mcp-server/src/main/resources/application.conf b/samples/kotlinlang-mcp-server/src/main/resources/application.conf new file mode 100644 index 000000000..4908da580 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/resources/application.conf @@ -0,0 +1,17 @@ +ktor { + deployment { + port = 8080 + port = ${?SERVER_PORT} + host = "0.0.0.0" + host = ${?SERVER_HOST} + } + application { + modules = [ org.kotlinlang.mcp.ApplicationKt.module ] + } +} + +algolia { + appId = ${ALGOLIA_APP_ID} + apiKey = ${ALGOLIA_API_KEY} + indexName = ${ALGOLIA_INDEX_NAME} +} diff --git a/samples/kotlinlang-mcp-server/src/main/resources/logback.xml b/samples/kotlinlang-mcp-server/src/main/resources/logback.xml new file mode 100644 index 000000000..0ffe8a34b --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/KotlinlangServerTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/KotlinlangServerTest.kt new file mode 100644 index 000000000..c6786fb34 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/KotlinlangServerTest.kt @@ -0,0 +1,62 @@ +package org.kotlinlang.mcp + +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.testing.ChannelTransport +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.kotlinlang.mcp.config.ServerConfig +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@OptIn(ExperimentalMcpApi::class) +class KotlinlangServerTest { + + private val testConfig = ServerConfig( + algoliaAppId = "test-app-id", + algoliaApiKey = "test-api-key", + algoliaIndexName = "test-index", + ) + + @Test + fun `server registers both tools with correct schema and annotations`() = runTest { + val kotlinlangServer = KotlinlangServer(testConfig) + kotlinlangServer.use { + val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + + val client = Client( + clientInfo = Implementation(name = "test-client", version = "1.0"), + options = ClientOptions(), + ) + + joinAll( + launch { client.connect(clientTransport) }, + launch { kotlinlangServer.server.createSession(serverTransport) }, + ) + + val tools = client.listTools().tools + + assertEquals(2, tools.size) + + val searchTool = tools.find { it.name == "search_kotlinlang" } + val searchProps = searchTool?.inputSchema?.properties + assertTrue(searchProps?.containsKey("query") ?: false) + assertEquals(listOf("query"), searchTool.inputSchema.required) + assertEquals(true, searchTool.annotations?.readOnlyHint) + assertEquals(true, searchTool.annotations?.openWorldHint) + + val pageTool = tools.find { it.name == "get_kotlinlang_page" } + val pageProps = pageTool?.inputSchema?.properties + assertTrue(pageProps?.containsKey("path") ?: false) + assertEquals(listOf("path"), pageTool.inputSchema.required) + assertEquals(true, pageTool.annotations?.readOnlyHint) + assertEquals(true, pageTool.annotations?.openWorldHint) + + client.close() + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClientTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClientTest.kt new file mode 100644 index 000000000..4f9dff6a0 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaClientTest.kt @@ -0,0 +1,166 @@ +package org.kotlinlang.mcp.algolia + +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.kotlinlang.mcp.config.ServerConfig +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class AlgoliaClientTest { + + private val testConfig = ServerConfig( + algoliaAppId = "test-app-id", + algoliaApiKey = "test-api-key", + algoliaIndexName = "test-index", + ) + + private val json = Json { ignoreUnknownKeys = true } + + private fun createClient(mockEngine: MockEngine): AlgoliaClient { + val httpClient = HttpClient(mockEngine) { + install(ContentNegotiation) { json(json) } + expectSuccess = true + } + return AlgoliaClient(testConfig, httpClient) + } + + @Test + fun `search sends correct URL and headers`() = runTest { + val mockEngine = MockEngine { request -> + assertEquals( + "https://test-app-id-dsn.algolia.net/1/indexes/test-index/query", + request.url.toString(), + ) + assertEquals("test-app-id", request.headers["x-algolia-application-id"]) + assertEquals("test-api-key", request.headers["x-algolia-api-key"]) + assertEquals(HttpMethod.Post, request.method) + + respond( + content = """{"hits": []}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + client.search("coroutines") + } + + @Test + fun `search sends correct request body`() = runTest { + val mockEngine = MockEngine { request -> + val body = json.decodeFromString(request.body.toByteArray().decodeToString()) + assertEquals("coroutines", body.query) + assertEquals(15, body.hitsPerPage) + assertEquals(listOf("objectID", "mainTitle", "url", "headings"), body.attributesToRetrieve) + assertEquals(listOf("content:40"), body.attributesToSnippet) + + respond( + content = """{"hits": []}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + client.search("coroutines") + } + + @Test + fun `search parses response with hits`() = runTest { + val responseJson = """ + { + "hits": [ + { + "objectID": "abc123", + "mainTitle": "Coroutines overview", + "url": "/docs/coroutines-overview.html", + "headings": "Introduction | First coroutine", + "_snippetResult": { + "content": { + "value": "Kotlin provides coroutines support", + "matchLevel": "full" + } + } + } + ] + } + """.trimIndent() + + val mockEngine = MockEngine { + respond( + content = responseJson, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + val result = client.search("coroutines") + + assertEquals(1, result.hits.size) + val hit = result.hits[0] + assertEquals("abc123", hit.objectID) + assertEquals("Coroutines overview", hit.mainTitle) + assertEquals("/docs/coroutines-overview.html", hit.url) + assertEquals("Introduction | First coroutine", hit.headings) + assertEquals("Kotlin provides coroutines support", hit.snippetResult?.content?.value) + assertEquals("full", hit.snippetResult?.content?.matchLevel) + } + + @Test + fun `search returns empty list when no hits`() = runTest { + val mockEngine = MockEngine { + respond( + content = """{"hits": []}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + val result = client.search("nonexistent") + + assertTrue(result.hits.isEmpty()) + } + + @Test + fun `search throws on 4xx error`() = runTest { + val mockEngine = MockEngine { + respond( + content = """{"message": "Invalid Application-ID or API key"}""", + status = HttpStatusCode.Forbidden, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + assertFailsWith { + client.search("coroutines") + } + } + + @Test + fun `search throws on 5xx error`() = runTest { + val mockEngine = MockEngine { + respond( + content = """{"message": "Internal server error"}""", + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + val client = createClient(mockEngine) + assertFailsWith { + client.search("coroutines") + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModelsTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModelsTest.kt new file mode 100644 index 000000000..10dbe5b88 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/algolia/AlgoliaModelsTest.kt @@ -0,0 +1,126 @@ +package org.kotlinlang.mcp.algolia + +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertEquals + +class AlgoliaModelsTest { + + private val json = Json { ignoreUnknownKeys = true } + + @Test + fun `serializes search request to JSON`() { + val request = AlgoliaSearchRequest( + query = "coroutines", + hitsPerPage = 15, + attributesToRetrieve = listOf("mainTitle", "url", "headings"), + attributesToSnippet = listOf("content:40"), + ) + + val encoded = json.encodeToString(request) + val decoded = json.decodeFromString(encoded) + + assertEquals(request, decoded) + } + + @Test + fun `parses full response with nested snippet`() { + val responseJson = """ + { + "hits": [ + { + "objectID": "abc123", + "mainTitle": "Coroutines overview", + "url": "/docs/coroutines-overview.html", + "headings": "Introduction | First coroutine", + "_snippetResult": { + "content": { + "value": "Kotlin provides coroutines support", + "matchLevel": "full" + } + } + } + ] + } + """.trimIndent() + + val response = json.decodeFromString(responseJson) + + assertEquals(1, response.hits.size) + val hit = response.hits[0] + assertEquals("abc123", hit.objectID) + assertEquals("Coroutines overview", hit.mainTitle) + assertEquals("/docs/coroutines-overview.html", hit.url) + assertEquals("Introduction | First coroutine", hit.headings) + assertEquals("Kotlin provides coroutines support", hit.snippetResult?.content?.value) + assertEquals("full", hit.snippetResult?.content?.matchLevel) + } + + @Test + fun `parses hit with missing nullable fields`() { + val responseJson = """ + { + "hits": [ + { + "objectID": "def456", + "url": "/docs/basic-syntax.html" + } + ] + } + """.trimIndent() + + val response = json.decodeFromString(responseJson) + + val hit = response.hits[0] + assertEquals("def456", hit.objectID) + assertEquals("/docs/basic-syntax.html", hit.url) + assertEquals(null, hit.mainTitle) + assertEquals(null, hit.headings) + assertEquals(null, hit.snippetResult) + } + + @Test + fun `parses response ignoring unknown fields`() { + val responseJson = """ + { + "hits": [ + { + "objectID": "ghi789", + "url": "/docs/functions.html", + "unknownField": 42, + "_snippetResult": { + "content": { + "value": "snippet text", + "matchLevel": "none", + "fullyHighlighted": false + }, + "anotherUnknown": "ignored" + } + } + ], + "nbHits": 100, + "page": 0, + "processingTimeMS": 5 + } + """.trimIndent() + + val response = json.decodeFromString(responseJson) + + assertEquals(1, response.hits.size) + assertEquals("ghi789", response.hits[0].objectID) + assertEquals("snippet text", response.hits[0].snippetResult?.content?.value) + } + + @Test + fun `parses response with empty hits list`() { + val responseJson = """ + { + "hits": [] + } + """.trimIndent() + + val response = json.decodeFromString(responseJson) + + assertEquals(emptyList(), response.hits) + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/cache/TtlCacheTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/cache/TtlCacheTest.kt new file mode 100644 index 000000000..a191fe715 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/cache/TtlCacheTest.kt @@ -0,0 +1,144 @@ +package org.kotlinlang.mcp.cache + +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.time.Duration.Companion.minutes +import kotlin.time.TestTimeSource + +class TtlCacheTest { + + @Test + fun `get returns value that was put`() { + val cache = TtlCache(ttl = 10.minutes) + cache.put("key", "value") + assertEquals("value", cache.get("key")) + } + + @Test + fun `get returns null for missing key`() { + val cache = TtlCache(ttl = 10.minutes) + assertNull(cache.get("missing")) + } + + @Test + fun `get returns null after TTL expires`() { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.put("key", "value") + assertEquals("value", cache.get("key")) + + timeSource += 11.minutes + assertNull(cache.get("key")) + } + + @Test + fun `get returns value just before TTL expires`() { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.put("key", "value") + timeSource += 9.minutes + assertEquals("value", cache.get("key")) + } + + @Test + fun `put overwrites value and resets TTL`() { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.put("key", "old") + timeSource += 8.minutes + + cache.put("key", "new") + timeSource += 8.minutes + + assertEquals("new", cache.get("key")) + } + + @Test + fun `getOrPut returns cached value without calling loader`() = runTest { + val cache = TtlCache(ttl = 10.minutes) + cache.put("key", "cached") + + var loaderCalled = false + val result = cache.getOrPut("key") { + loaderCalled = true + "loaded" + } + + assertEquals("cached", result) + assertEquals(false, loaderCalled) + } + + @Test + fun `getOrPut calls loader on cache miss and caches result`() = runTest { + val cache = TtlCache(ttl = 10.minutes) + + val result = cache.getOrPut("key") { "loaded" } + + assertEquals("loaded", result) + assertEquals("loaded", cache.get("key")) + } + + @Test + fun `getOrPut calls loader again after TTL expires`() = runTest { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.getOrPut("key") { "first" } + timeSource += 11.minutes + + val result = cache.getOrPut("key") { "second" } + assertEquals("second", result) + } + + @Test + fun `put evicts expired entries`() { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.put("a", "1") + cache.put("b", "2") + timeSource += 11.minutes + + // Both entries are expired but still in memory. + // A new put triggers eviction of expired entries. + cache.put("c", "3") + + assertNull(cache.get("a")) + assertNull(cache.get("b")) + assertEquals("3", cache.get("c")) + } + + @Test + fun `put does not evict non-expired entries`() { + val timeSource = TestTimeSource() + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + + cache.put("a", "1") + timeSource += 5.minutes + cache.put("b", "2") + + assertEquals("1", cache.get("a")) + assertEquals("2", cache.get("b")) + } + + @Test + fun `concurrent put and get do not throw`() = runTest { + val cache = TtlCache(ttl = 10.minutes) + val jobs = (1..100).map { i -> + launch { + cache.put(i, "value-$i") + cache.get(i) + cache.put(i, "updated-$i") + cache.get(i) + } + } + jobs.joinAll() + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/config/ServerConfigTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/config/ServerConfigTest.kt new file mode 100644 index 000000000..571460267 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/config/ServerConfigTest.kt @@ -0,0 +1,35 @@ +package org.kotlinlang.mcp.config + +import io.ktor.server.config.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class ServerConfigTest { + + @Test + fun `toServerConfig maps all Algolia properties`() { + val config = MapApplicationConfig( + "algolia.appId" to "test-app-id", + "algolia.apiKey" to "test-api-key", + "algolia.indexName" to "test-index", + ) + + val serverConfig = config.toServerConfig() + + assertEquals("test-app-id", serverConfig.algoliaAppId) + assertEquals("test-api-key", serverConfig.algoliaApiKey) + assertEquals("test-index", serverConfig.algoliaIndexName) + } + + @Test + fun `toServerConfig throws when required property is missing`() { + val config = MapApplicationConfig( + "algolia.appId" to "test-app-id", + ) + + assertFailsWith { + config.toServerConfig() + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/PageFetcherTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/PageFetcherTest.kt new file mode 100644 index 000000000..1cd3e6b24 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/PageFetcherTest.kt @@ -0,0 +1,87 @@ +package org.kotlinlang.mcp.content + +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.client.plugins.* +import io.ktor.http.* +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class PageFetcherTest { + + private fun createFetcher(mockEngine: MockEngine): PageFetcher { + val httpClient = HttpClient(mockEngine) { + expectSuccess = true + } + return PageFetcher(httpClient) + } + + @Test + fun `fetch sends GET request to provided URL`() = runTest { + val mockEngine = MockEngine { request -> + assertEquals("https://kotlinlang.org/docs/_llms/coroutines-overview.txt", request.url.toString()) + assertEquals(HttpMethod.Get, request.method) + + respond( + content = "# Coroutines overview", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + + val fetcher = createFetcher(mockEngine) + fetcher.fetch("https://kotlinlang.org/docs/_llms/coroutines-overview.txt") + } + + @Test + fun `fetch returns response body as text`() = runTest { + val pageContent = "# Coroutines\n\nKotlin provides coroutines support at the language level." + + val mockEngine = MockEngine { + respond( + content = pageContent, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + + val fetcher = createFetcher(mockEngine) + val result = fetcher.fetch("https://kotlinlang.org/docs/_llms/coroutines-overview.txt") + + assertEquals(pageContent, result) + } + + @Test + fun `fetch throws ClientRequestException on 404`() = runTest { + val mockEngine = MockEngine { + respond( + content = "Not Found", + status = HttpStatusCode.NotFound, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + + val fetcher = createFetcher(mockEngine) + assertFailsWith { + fetcher.fetch("https://kotlinlang.org/docs/_llms/nonexistent.txt") + } + } + + @Test + fun `fetch throws ServerResponseException on 5xx`() = runTest { + val mockEngine = MockEngine { + respond( + content = "Internal Server Error", + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + + val fetcher = createFetcher(mockEngine) + assertFailsWith { + fetcher.fetch("https://kotlinlang.org/docs/_llms/coroutines-overview.txt") + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/UrlMapperTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/UrlMapperTest.kt new file mode 100644 index 000000000..c2076a303 --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/content/UrlMapperTest.kt @@ -0,0 +1,91 @@ +package org.kotlinlang.mcp.content + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class UrlMapperTest { + + @Test + fun `top-level path maps to _llms URL`() { + assertEquals( + "https://kotlinlang.org/docs/_llms/coroutines-overview.txt", + mapPathToUrl("coroutines-overview"), + ) + } + + @Test + fun `nested path maps to _llms URL`() { + assertEquals( + "https://kotlinlang.org/docs/multiplatform/_llms/compose.txt", + mapPathToUrl("multiplatform/compose"), + ) + } + + @Test + fun `deep nested path maps to _llms URL`() { + assertEquals( + "https://kotlinlang.org/docs/a/b/_llms/c.txt", + mapPathToUrl("a/b/c"), + ) + } + + @Test + fun `trims leading and trailing slashes`() { + assertEquals( + "https://kotlinlang.org/docs/_llms/coroutines-overview.txt", + mapPathToUrl("/coroutines-overview/"), + ) + } + + @Test + fun `strips html extension`() { + assertEquals( + "https://kotlinlang.org/docs/_llms/coroutines-overview.txt", + mapPathToUrl("coroutines-overview.html"), + ) + } + + @Test + fun `normalizes slashes and html extension together`() { + assertEquals( + "https://kotlinlang.org/docs/multiplatform/_llms/compose.txt", + mapPathToUrl("/multiplatform/compose.html"), + ) + } + + @Test + fun `empty string throws IllegalArgumentException`() { + assertFailsWith { + mapPathToUrl("") + } + } + + @Test + fun `only slashes throws IllegalArgumentException`() { + assertFailsWith { + mapPathToUrl("///") + } + } + + @Test + fun `blank path throws IllegalArgumentException`() { + assertFailsWith { + mapPathToUrl(" ") + } + } + + @Test + fun `path traversal throws IllegalArgumentException`() { + assertFailsWith { + mapPathToUrl("../../secret") + } + } + + @Test + fun `path traversal in the middle throws IllegalArgumentException`() { + assertFailsWith { + mapPathToUrl("multiplatform/../secret") + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPageTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPageTest.kt new file mode 100644 index 000000000..18f0cfe1e --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/GetKotlinlangPageTest.kt @@ -0,0 +1,139 @@ +package org.kotlinlang.mcp.tools + +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.http.* +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import kotlinx.coroutines.test.runTest +import org.kotlinlang.mcp.cache.TtlCache +import org.kotlinlang.mcp.content.PageFetcher +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.hours + +class GetKotlinlangPageTest { + + private fun createTool(mockEngine: MockEngine): GetKotlinlangPage { + val httpClient = HttpClient(mockEngine) { + expectSuccess = true + } + val pageFetcher = PageFetcher(httpClient) + val cache = TtlCache(ttl = 1.hours) + return GetKotlinlangPage(pageFetcher, cache) + } + + @Test + fun `handle returns page content for valid path`() = runTest { + val pageContent = "# Coroutines\n\nKotlin provides coroutines support." + val mockEngine = MockEngine { request -> + assertEquals( + "https://kotlinlang.org/docs/_llms/coroutines-overview.txt", + request.url.toString(), + ) + respond( + content = pageContent, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + val tool = createTool(mockEngine) + + val result = tool.handle("coroutines-overview") + val text = (result.content.first() as TextContent).text + + assertNull(result.isError) + assertEquals(pageContent, text) + } + + @Test + fun `handle returns cached content on second call`() = runTest { + var callCount = 0 + val mockEngine = MockEngine { + callCount++ + respond( + content = "# Page content", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + val tool = createTool(mockEngine) + + val result1 = tool.handle("coroutines-overview") + val result2 = tool.handle("coroutines-overview") + + assertEquals(1, callCount) + assertEquals( + (result1.content.first() as TextContent).text, + (result2.content.first() as TextContent).text, + ) + } + + @Test + fun `handle returns error with hint on 404`() = runTest { + val mockEngine = MockEngine { + respond( + content = "Not Found", + status = HttpStatusCode.NotFound, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + val tool = createTool(mockEngine) + + val result = tool.handle("nonexistent-page") + val text = (result.content.first() as TextContent).text + + assertEquals(true, result.isError) + assertTrue(text.contains("Page not found: nonexistent-page")) + assertTrue(text.contains("search_kotlinlang")) + } + + @Test + fun `handle returns error on network failure`() = runTest { + val mockEngine = MockEngine { + respond( + content = "Internal Server Error", + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + val tool = createTool(mockEngine) + + val result = tool.handle("coroutines-overview") + + assertEquals(true, result.isError) + val text = (result.content.first() as TextContent).text + assertTrue(text.startsWith("Failed to fetch page:")) + } + + @Test + fun `handle returns error on empty path`() = runTest { + val mockEngine = MockEngine { + respond( + content = "should not reach", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "text/plain"), + ) + } + val tool = createTool(mockEngine) + + val result = tool.handle("") + + assertEquals(true, result.isError) + val text = (result.content.first() as TextContent).text + assertTrue(text.contains("Invalid path:")) + } + + @Test + fun `handle rethrows CancellationException`() = runTest { + val mockEngine = MockEngine { + throw kotlinx.coroutines.CancellationException("cancelled") + } + val tool = createTool(mockEngine) + + kotlin.test.assertFailsWith { + tool.handle("coroutines-overview") + } + } +} diff --git a/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlangTest.kt b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlangTest.kt new file mode 100644 index 000000000..7e10420ae --- /dev/null +++ b/samples/kotlinlang-mcp-server/src/test/kotlin/org/kotlinlang/mcp/tools/SearchKotlinlangTest.kt @@ -0,0 +1,231 @@ +package org.kotlinlang.mcp.tools + +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.kotlinlang.mcp.algolia.AlgoliaClient +import org.kotlinlang.mcp.cache.TtlCache +import org.kotlinlang.mcp.config.ServerConfig +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.minutes +import kotlin.time.TestTimeSource + +class SearchKotlinlangTest { + + private val testConfig = ServerConfig( + algoliaAppId = "test-app-id", + algoliaApiKey = "test-api-key", + algoliaIndexName = "test-index", + ) + + private val json = Json { ignoreUnknownKeys = true } + + private fun createSearchTool( + mockEngine: MockEngine, + timeSource: TestTimeSource = TestTimeSource(), + ): SearchKotlinlang { + val httpClient = HttpClient(mockEngine) { + install(ContentNegotiation) { json(json) } + } + val algoliaClient = AlgoliaClient(testConfig, httpClient) + val cache = TtlCache(ttl = 10.minutes, timeSource = timeSource) + return SearchKotlinlang(algoliaClient, cache) + } + + private fun mockEngineWithHits(hitsJson: String): MockEngine = MockEngine { + respond( + content = """{"hits": [$hitsJson]}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + + private fun hit( + objectID: String = "id1", + mainTitle: String? = "Title", + url: String = "/docs/test.html", + snippet: String? = "some text", + matchLevel: String = "full", + ): String = buildString { + append("""{"objectID":"$objectID","url":"$url"""") + if (mainTitle != null) append(""","mainTitle":"$mainTitle"""") + if (snippet != null) { + append(""","_snippetResult":{"content":{"value":"$snippet","matchLevel":"$matchLevel"}}""") + } + append("}") + } + + @Test + fun `filters out non-docs hits`() = runTest { + val engine = mockEngineWithHits( + listOf( + hit(objectID = "1", url = "/docs/coroutines.html", mainTitle = "Coroutines"), + hit(objectID = "2", url = "https://kotlinlang.org/api/core/kotlin/", mainTitle = "API"), + hit(objectID = "3", url = "/docs/flow.html", mainTitle = "Flow"), + ).joinToString(",") + ) + val tool = createSearchTool(engine) + + val result = tool.handle("coroutines") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertTrue(text.contains("Coroutines")) + assertTrue(text.contains("Flow")) + assertTrue(!text.contains("API")) + } + + @Test + fun `takes only top 5 after filtering`() = runTest { + val hits = (1..10).map { i -> + hit(objectID = "id$i", url = "/docs/page$i.html", mainTitle = "Page $i", snippet = "text $i") + } + val engine = mockEngineWithHits(hits.joinToString(",")) + val tool = createSearchTool(engine) + + val result = tool.handle("kotlin") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + for (i in 1..5) assertTrue(text.contains("Page $i"), "Should contain Page $i") + for (i in 6..10) assertTrue(!text.contains("Page $i"), "Should not contain Page $i") + } + + @Test + fun `formats results with title path and snippet`() = runTest { + val engine = mockEngineWithHits( + hit( + objectID = "1", + url = "/docs/coroutines-overview.html", + mainTitle = "Coroutines overview", + snippet = "Kotlin provides coroutines support", + ) + ) + val tool = createSearchTool(engine) + + val result = tool.handle("coroutines") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertEquals( + """ + 1. Coroutines overview [coroutines-overview] + Kotlin provides coroutines support + """.trimIndent(), + text, + ) + } + + @Test + fun `strips HTML tags from snippet`() = runTest { + val engine = mockEngineWithHits( + hit(snippet = "coroutines are great") + ) + val tool = createSearchTool(engine) + + val result = tool.handle("coroutines") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertTrue(text.contains("coroutines are great")) + assertTrue(!text.contains("")) + assertTrue(!text.contains("")) + } + + @Test + fun `uses Untitled when mainTitle is null`() = runTest { + val engine = mockEngineWithHits( + hit(mainTitle = null, url = "/docs/basics.html", snippet = "some text") + ) + val tool = createSearchTool(engine) + + val result = tool.handle("basics") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertTrue(text.contains("Untitled [basics]")) + } + + @Test + fun `omits snippet line when snippetResult is null`() = runTest { + val engine = mockEngineWithHits( + hit(url = "/docs/basics.html", mainTitle = "Basics", snippet = null) + ) + val tool = createSearchTool(engine) + + val result = tool.handle("basics") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertEquals("1. Basics [basics]", text) + } + + @Test + fun `returns no results message when all hits are filtered out`() = runTest { + val engine = mockEngineWithHits( + hit(url = "https://kotlinlang.org/api/core/kotlin/", mainTitle = "API") + ) + val tool = createSearchTool(engine) + + val result = tool.handle("something") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertNull(result.isError) + assertEquals("No results found for: something", text) + } + + @Test + fun `returns isError true when Algolia fails`() = runTest { + val engine = MockEngine { + respond( + content = """{"message": "Internal server error"}""", + status = HttpStatusCode.InternalServerError, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + val tool = createSearchTool(engine) + + val result = tool.handle("coroutines") + + assertEquals(true, result.isError) + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + assertTrue(text.startsWith("Search failed:")) + } + + @Test + fun `caches result and does not call Algolia on second request`() = runTest { + var callCount = 0 + val engine = MockEngine { + callCount++ + respond( + content = """{"hits": [${hit(url = "/docs/test.html")}]}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + val tool = createSearchTool(engine) + + tool.handle("coroutines") + tool.handle("coroutines") + + assertEquals(1, callCount) + } + + @Test + fun `extracts path from nested URL`() = runTest { + val engine = mockEngineWithHits( + hit( + url = "/docs/multiplatform/compose.html", + mainTitle = "Compose", + snippet = "text", + ) + ) + val tool = createSearchTool(engine) + + val result = tool.handle("compose") + val text = result.content.first().let { (it as io.modelcontextprotocol.kotlin.sdk.types.TextContent).text } + + assertTrue(text.contains("[multiplatform/compose]")) + } +} From 18f4c477ef22b7d06dcb8d49e9baa81601adfbb8 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 27 Apr 2026 16:27:41 +0200 Subject: [PATCH 105/187] chore: remove idea files related to detekt (#714) - detekt idea plugin version (1.23.8) is incompatible with the gradle plugin version (2.0.0-alpha.2) - remove the annoying popup shown when the idea starts image --- .idea/detekt.xml | 9 --------- .idea/externalDependencies.xml | 7 ------- .idea/ktlint-plugin.xml | 7 ------- 3 files changed, 23 deletions(-) delete mode 100644 .idea/detekt.xml delete mode 100644 .idea/externalDependencies.xml delete mode 100644 .idea/ktlint-plugin.xml diff --git a/.idea/detekt.xml b/.idea/detekt.xml deleted file mode 100644 index d89f9080f..000000000 --- a/.idea/detekt.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/.idea/externalDependencies.xml b/.idea/externalDependencies.xml deleted file mode 100644 index 0a081df94..000000000 --- a/.idea/externalDependencies.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/.idea/ktlint-plugin.xml b/.idea/ktlint-plugin.xml deleted file mode 100644 index e8bd90cf2..000000000 --- a/.idea/ktlint-plugin.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - DISTRACT_FREE - DEFAULT - - \ No newline at end of file From 5b4ea4b774cb75d36264e4951d8e990b2e47d895 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:03:48 +0200 Subject: [PATCH 106/187] chore(deps): bump the kotlin group across 1 directory with 2 updates (#723) Bumps the kotlin group with 2 updates in the / directory: [org.jetbrains.kotlin:kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) and [org.jetbrains.kotlin:kotlin-serialization](https://github.com/JetBrains/kotlin). Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 2.3.20 to 2.3.21

Release notes

Sourced from org.jetbrains.kotlin:kotlin-gradle-plugin's releases.

Kotlin 2.3.21

Changelog

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin:kotlin-gradle-plugin's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'
Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlin:kotlin-serialization` from 2.3.20 to 2.3.21
Release notes

Sourced from org.jetbrains.kotlin:kotlin-serialization's releases.

Kotlin 2.3.21

Changelog

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin:kotlin-serialization's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'
Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlin:kotlin-serialization` from 2.3.20 to 2.3.21
Release notes

Sourced from org.jetbrains.kotlin:kotlin-serialization's releases.

Kotlin 2.3.21

Changelog

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin:kotlin-serialization's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'
Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

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 36478a99e..ca67d7e56 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ atomicfu = "0.32.1" binaryCompatibilityValidatorPlugin = "0.18.1" detekt = "2.0.0-alpha.2" dokka = "2.2.0" -kotlin = "2.3.20" +kotlin = "2.3.21" kover = "0.9.8" ktlint = "14.2.0" knit = "0.5.1" From 0835fbd7ed62173c5ee78ca2075eb95178c2f825 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:31:26 +0000 Subject: [PATCH 107/187] chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/simple-streamable-server (#725) Bumps `kotlin` from 2.2.21 to 2.3.21. Updates `org.jetbrains.kotlin.jvm` from 2.2.21 to 2.3.21
Release notes

Sourced from org.jetbrains.kotlin.jvm's releases.

Kotlin 2.3.21

Changelog

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin.jvm's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'

... (truncated)

Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.2.21 to 2.3.21
Release notes

Sourced from org.jetbrains.kotlin.plugin.serialization's releases.

Kotlin 2.3.21

Changelog

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin.plugin.serialization's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'

... (truncated)

Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/simple-streamable-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index 0cca0267f..dc668c0d1 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.2.21" +kotlin = "2.3.21" ktor = "3.3.3" junit = "6.0.3" mcp-kotlin = "0.11.1" From 3c917cc97a97f9a8aea1169a30308c67a2cdf338 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:21:38 +0000 Subject: [PATCH 108/187] chore(deps): bump com.anthropic:anthropic-java from 2.26.0 to 2.27.0 in /samples/kotlin-mcp-client in the other-dependencies group across 1 directory (#719) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-client directory: [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java). Updates `com.anthropic:anthropic-java` from 2.26.0 to 2.27.0
Release notes

Sourced from com.anthropic:anthropic-java's releases.

v2.27.0

2.27.0 (2026-04-23)

Full Changelog: v2.26.0...v2.27.0

Features

  • api: CMA Memory public beta (d95526a)

Bug Fixes

  • api: fix errors in api spec (9d5a18a)
  • api: restore missing features (e737fbf)

Chores

Changelog

Sourced from com.anthropic:anthropic-java's changelog.

2.27.0 (2026-04-23)

Full Changelog: v2.26.0...v2.27.0

Features

  • api: CMA Memory public beta (d95526a)

Bug Fixes

  • api: fix errors in api spec (9d5a18a)
  • api: restore missing features (e737fbf)

Chores

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 5de32f0b1..bbb6bb790 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -anthropic = "2.26.0" +anthropic = "2.27.0" kotlin = "2.2.21" ktor = "3.3.3" mcp-kotlin = "0.11.1" From 9817b174f9e1a4f3ebf6855e41ff666b53a04b1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:39:46 +0000 Subject: [PATCH 109/187] chore(deps): bump org.jetbrains.kotlin.jvm from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-client (#721) Bumps [org.jetbrains.kotlin.jvm](https://github.com/JetBrains/kotlin) from 2.2.21 to 2.3.21.
Changelog

Sourced from org.jetbrains.kotlin.jvm's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'

... (truncated)

Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index bbb6bb790..69ebddef5 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] anthropic = "2.27.0" -kotlin = "2.2.21" +kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.11.1" shadow = "9.4.1" From 1d9304264e3a9d81e0d84511fdae683abc7f6fef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:30:51 +0200 Subject: [PATCH 110/187] chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-server (#722) Bumps `kotlin` from 2.2.21 to 2.3.21. Updates `org.jetbrains.kotlin.jvm` from 2.2.21 to 2.3.21
Release notes

Sourced from org.jetbrains.kotlin.jvm's releases.

Kotlin 2.3.21

Changelog

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin.jvm's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'

... (truncated)

Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.2.21 to 2.3.21
Release notes

Sourced from org.jetbrains.kotlin.plugin.serialization's releases.

Kotlin 2.3.21

Changelog

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin.plugin.serialization's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'

... (truncated)

Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index 3a03aed06..a6c07dc00 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.2.21" +kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.11.1" slf4j = "2.0.17" From ec606dbc2483e80d2a82a6b27b1588a828f29d9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:07:10 +0200 Subject: [PATCH 111/187] chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-server (#726) Bumps `kotlin` from 2.2.21 to 2.3.21. Updates `org.jetbrains.kotlin.jvm` from 2.2.21 to 2.3.21
Changelog

Sourced from org.jetbrains.kotlin.jvm's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'

... (truncated)

Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.2.21 to 2.3.21
Changelog

Sourced from org.jetbrains.kotlin.plugin.serialization's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'

... (truncated)

Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index 839a283d8..19874f44f 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] coroutines = "1.10.2" -kotlin = "2.2.21" +kotlin = "2.3.21" ktor = "3.3.3" logging = "8.0.01" mcp-kotlin = "0.11.1" From 12ace760cb9e40c820be14ccd6f278f08e1115b5 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 28 Apr 2026 17:10:11 +0200 Subject: [PATCH 112/187] chore: remove redundant JUnit XML report configuration (#728) The `junitXml.required`, `includeSystemOutLog` and `includeSystemErrLog` properties all default to `true` in Gradle JUnitXmlReport, so setting them explicitly was a no-op. The latter two are also @Incubating API --- buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts index a3311394c..2903cff6a 100644 --- a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts +++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts @@ -45,11 +45,6 @@ kotlin { events("failed") } systemProperty("kotest.output.ansi", "true") - reports { - junitXml.required.set(true) - junitXml.includeSystemOutLog.set(true) - junitXml.includeSystemErrLog.set(true) - } } } macosArm64() From 7474a33c5641b0096ffd84d7aeb9181f4aa976fb Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 28 Apr 2026 17:30:32 +0200 Subject: [PATCH 113/187] chore: drop deprecated watchosX64 and tvosX64 targets (#727) Drop the deprecated `watchosX64` and `tvosX64` ## Breaking Changes consumers that depended on `watchosX64` or `tvosX64` artifacts will need to migrate to the corresponding ARM/simulator targets. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed --- kotlin-sdk-client/build.gradle.kts | 2 -- kotlin-sdk-core/build.gradle.kts | 2 -- kotlin-sdk-testing/build.gradle.kts | 2 -- test-utils/build.gradle.kts | 2 -- 4 files changed, 8 deletions(-) diff --git a/kotlin-sdk-client/build.gradle.kts b/kotlin-sdk-client/build.gradle.kts index f50266c99..a3645b768 100644 --- a/kotlin-sdk-client/build.gradle.kts +++ b/kotlin-sdk-client/build.gradle.kts @@ -14,10 +14,8 @@ kotlin { iosArm64() iosX64() iosSimulatorArm64() - watchosX64() watchosArm64() watchosSimulatorArm64() - tvosX64() tvosArm64() tvosSimulatorArm64() js { diff --git a/kotlin-sdk-core/build.gradle.kts b/kotlin-sdk-core/build.gradle.kts index f4a231483..967dae315 100644 --- a/kotlin-sdk-core/build.gradle.kts +++ b/kotlin-sdk-core/build.gradle.kts @@ -35,10 +35,8 @@ kotlin { iosArm64() iosX64() iosSimulatorArm64() - watchosX64() watchosArm64() watchosSimulatorArm64() - tvosX64() tvosArm64() tvosSimulatorArm64() js { diff --git a/kotlin-sdk-testing/build.gradle.kts b/kotlin-sdk-testing/build.gradle.kts index 902665c12..e318f6f09 100644 --- a/kotlin-sdk-testing/build.gradle.kts +++ b/kotlin-sdk-testing/build.gradle.kts @@ -14,10 +14,8 @@ kotlin { iosArm64() iosX64() iosSimulatorArm64() - watchosX64() watchosArm64() watchosSimulatorArm64() - tvosX64() tvosArm64() tvosSimulatorArm64() js { diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts index 91574e023..6e9125717 100644 --- a/test-utils/build.gradle.kts +++ b/test-utils/build.gradle.kts @@ -11,10 +11,8 @@ kotlin { iosArm64() iosX64() iosSimulatorArm64() - watchosX64() watchosArm64() watchosSimulatorArm64() - tvosX64() tvosArm64() tvosSimulatorArm64() js { From f434fcaff13777dcdf9d876979ff6b0b3f4507f2 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 28 Apr 2026 17:51:48 +0200 Subject: [PATCH 114/187] feat!: SEP-1577 sampling with tools (#718) Implements SEP-1577 sampling With tools closes #409 ## How Has This Been Tested? unit + integration ## Breaking Changes - binary changes - MediaContent -> SamplingMessageContent - ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .../sdk/conformance/ConformanceTools.kt | 4 +- .../kotlin/sdk/client/ClientTest.kt | 16 +- .../kotlin/sdk/server/SamplingTest.kt | 251 ++++++++++++++++++ kotlin-sdk-client/api/kotlin-sdk-client.api | 1 + .../kotlin/sdk/client/Client.kt | 24 ++ .../kotlin/sdk/client/SamplingValidation.kt | 24 ++ .../client/ClientSamplingValidationTest.kt | 98 +++++++ kotlin-sdk-core/api/kotlin-sdk-core.api | 219 +++++++++++++-- .../kotlin/sdk/shared/Protocol.kt | 17 +- .../kotlin/sdk/types/capabilities.dsl.kt | 30 +-- .../kotlin/sdk/types/capabilities.kt | 47 +++- .../kotlin/sdk/types/content.kt | 73 ++++- .../kotlin/sdk/types/initialize.dsl.kt | 8 +- .../kotlin/sdk/types/sampling.dsl.kt | 8 +- .../kotlin/sdk/types/sampling.kt | 89 ++++++- .../kotlin/sdk/types/serializers.kt | 100 +++++++ .../kotlin/sdk/types/CapabilitiesTest.kt | 63 ++++- .../kotlin/sdk/types/ContentTest.kt | 138 ++++++++++ .../kotlin/sdk/types/InitializeTest.kt | 2 +- .../kotlin/sdk/types/SamplingTest.kt | 238 ++++++++++++++++- .../sdk/types/dsl/CapabilitiesDslTest.kt | 18 +- .../kotlin/sdk/types/dsl/ContentDslTest.kt | 24 +- .../kotlin/sdk/types/dsl/InitializeDslTest.kt | 15 +- .../kotlin/sdk/types/dsl/SamplingDslTest.kt | 12 +- .../kotlin/sdk/server/ClientConnection.kt | 32 ++- .../kotlin/sdk/server/SamplingValidation.kt | 51 ++++ .../kotlin/sdk/server/SamplingTest.kt | 86 ++++++ 27 files changed, 1553 insertions(+), 135 deletions(-) create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt create mode 100644 kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt create mode 100644 kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt create mode 100644 kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt create mode 100644 kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt index 54d5c8c44..dc126f681 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt @@ -176,11 +176,11 @@ fun Server.registerConformanceTools() { CreateMessageRequest( CreateMessageRequestParams( maxTokens = 10000, - messages = listOf(SamplingMessage(Role.User, TextContent(prompt))), + messages = listOf(SamplingMessage(Role.User, listOf(TextContent(prompt)))), ), ), ) - CallToolResult(listOf(TextContent(result.content.toString()))) + CallToolResult(listOf(TextContent(result.content.joinToString("\n") { it.toString() }))) } // 9. Elicitation diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index d0acda59d..e5f66383a 100644 --- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -106,7 +106,7 @@ class ClientTest { ), options = ClientOptions( capabilities = ClientCapabilities( - sampling = EmptyJsonObject, + sampling = ClientCapabilities.sampling, ), ), ) @@ -152,7 +152,7 @@ class ClientTest { ), options = ClientOptions( capabilities = ClientCapabilities( - sampling = EmptyJsonObject, + sampling = ClientCapabilities.sampling, ), ), ) @@ -339,7 +339,7 @@ class ClientTest { val client = Client( clientInfo = Implementation(name = "test client", version = "1.0"), options = ClientOptions( - capabilities = ClientCapabilities(sampling = EmptyJsonObject), + capabilities = ClientCapabilities(sampling = ClientCapabilities.sampling), ), ) @@ -633,18 +633,16 @@ class ClientTest { ), options = ClientOptions( capabilities = ClientCapabilities( - sampling = EmptyJsonObject, + sampling = ClientCapabilities.sampling, ), ), ) client.setRequestHandler(Method.Defined.SamplingCreateMessage) { _, _ -> CreateMessageResult( - model = "test-model", role = Role.Assistant, - content = TextContent( - text = "Test response", - ), + content = TextContent(text = "Test response"), + model = "test-model", ) } @@ -670,7 +668,7 @@ class ClientTest { val client = Client( clientInfo = Implementation(name = "test client", version = "1.0"), options = ClientOptions( - capabilities = ClientCapabilities(sampling = EmptyJsonObject), + capabilities = ClientCapabilities(sampling = ClientCapabilities.sampling), ), ) diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt new file mode 100644 index 000000000..f161d8ec0 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt @@ -0,0 +1,251 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.IncludeContext +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.StopReason +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.types.ToolChoice +import io.modelcontextprotocol.kotlin.sdk.types.ToolResultContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.sdk.types.ToolUseContent +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import org.junit.jupiter.api.assertDoesNotThrow +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class SamplingTest { + + private val dummyTool = Tool( + name = "t", + inputSchema = ToolSchema(properties = buildJsonObject { }, required = emptyList()), + ) + + private val weatherTool = Tool( + name = "get_weather", + description = "Return the current temperature in Celsius.", + inputSchema = ToolSchema( + properties = buildJsonObject { + put("location", buildJsonObject { put("type", JsonPrimitive("string")) }) + }, + required = listOf("location"), + ), + ) + + private val minimalMessages = listOf(SamplingMessage(Role.User, TextContent("hi"))) + + /** + * Builds a connected [Server]+[Client] pair using [InMemoryTransport]. + * + * @param clientCapabilities the capabilities the client advertises during initialize + * @param samplingHandler the handler the client uses to respond to sampling requests + * @return Pair of (server, sessionId) ready for [Server.createMessage] calls. + */ + private fun buildPair( + clientCapabilities: ClientCapabilities = ClientCapabilities( + sampling = ClientCapabilities.Sampling(), + ), + samplingHandler: (CreateMessageRequest) -> CreateMessageResult = { _ -> + CreateMessageResult(role = Role.Assistant, content = TextContent("ok"), model = "m") + }, + ): Pair { + val server = Server( + serverInfo = Implementation(name = "srv", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) + + val client = Client( + clientInfo = Implementation(name = "cli", version = "1.0"), + options = ClientOptions(capabilities = clientCapabilities), + ) + + client.setRequestHandler(Method.Defined.SamplingCreateMessage) { req, _ -> + samplingHandler(req) + } + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + var sessionId: String? = null + runBlocking { + val sessionDeferred = CompletableDeferred() + launch { client.connect(clientTransport) } + launch { + val session = server.createSession(serverTransport) + sessionDeferred.complete(session.sessionId) + } + sessionId = sessionDeferred.await() + } + + return Pair(server, checkNotNull(sessionId)) + } + + // ============================================================================ + // Server.createMessage — capability enforcement (SEP-1577) + // ============================================================================ + + @Test + fun `tools field rejected when client has no sampling tools capability`() { + val (server, sessionId) = buildPair() + assertFailsWith { + runBlocking { + server.createMessage( + sessionId = sessionId, + params = CreateMessageRequest( + params = CreateMessageRequestParams( + maxTokens = 100, + messages = minimalMessages, + tools = listOf(dummyTool), + ), + ), + ) + } + } + } + + @Test + fun `toolChoice field rejected when client has no sampling tools capability`() { + val (server, sessionId) = buildPair() + assertFailsWith { + runBlocking { + server.createMessage( + sessionId = sessionId, + params = CreateMessageRequest( + params = CreateMessageRequestParams( + maxTokens = 100, + messages = minimalMessages, + toolChoice = ToolChoice(), + ), + ), + ) + } + } + } + + @Test + fun `includeContext with no sampling context capability succeeds with a warning`() { + val (server, sessionId) = buildPair() + assertDoesNotThrow { + runBlocking { + server.createMessage( + sessionId = sessionId, + params = CreateMessageRequest( + params = CreateMessageRequestParams( + maxTokens = 100, + messages = minimalMessages, + includeContext = IncludeContext.ThisServer, + ), + ), + ) + } + } + } + + // ============================================================================ + // End-to-end tool-loop integration + // ============================================================================ + + @Test + fun `server sends tools, client returns tool_use then final text`() { + var turn = 0 + val (server, sessionId) = buildPair( + clientCapabilities = ClientCapabilities( + sampling = ClientCapabilities.Sampling(tools = EmptyJsonObject), + ), + samplingHandler = { _ -> + turn++ + if (turn == 1) { + CreateMessageResult( + role = Role.Assistant, + content = listOf( + TextContent("Let me check."), + ToolUseContent( + id = "call_1", + name = "get_weather", + input = buildJsonObject { put("location", JsonPrimitive("London")) }, + ), + ), + model = "test", + stopReason = StopReason.ToolUse, + ) + } else { + CreateMessageResult( + role = Role.Assistant, + content = TextContent("The temperature in London is 20°C."), + model = "test", + stopReason = StopReason.EndTurn, + ) + } + }, + ) + + runBlocking { + val messages = mutableListOf( + SamplingMessage(Role.User, TextContent("What is the weather in London?")), + ) + + // — Turn 1: server sends tools, expects tool_use stop reason + val first = server.createMessage( + sessionId = sessionId, + params = CreateMessageRequest( + params = CreateMessageRequestParams( + maxTokens = 256, + messages = messages.toList(), + tools = listOf(weatherTool), + toolChoice = ToolChoice(mode = ToolChoice.Mode.Auto), + ), + ), + ) + assertEquals(StopReason.ToolUse, first.stopReason) + assertEquals(2, first.content.size) + + // — Append assistant turn and inject tool result + messages.add(SamplingMessage(Role.Assistant, first.content)) + val toolUse = first.content.filterIsInstance().single() + messages.add( + SamplingMessage( + Role.User, + ToolResultContent( + toolUseId = toolUse.id, + content = listOf(TextContent("""{"tempC":20}""")), + ), + ), + ) + + // — Turn 2: server sends updated history, expects final text + val second = server.createMessage( + sessionId = sessionId, + params = CreateMessageRequest( + params = CreateMessageRequestParams( + maxTokens = 256, + messages = messages.toList(), + tools = listOf(weatherTool), + toolChoice = ToolChoice(mode = ToolChoice.Mode.Auto), + ), + ), + ) + assertEquals(StopReason.EndTurn, second.stopReason) + assertEquals(1, second.content.size) + val text = second.content.single() as TextContent + assertEquals("The temperature in London is 20°C.", text.text) + + server.close() + } + } +} diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index da0e752a7..8b6fb79b4 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -41,6 +41,7 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextp public static synthetic fun subscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun unsubscribeResource (Lio/modelcontextprotocol/kotlin/sdk/types/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/types/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + protected fun wrapRequestHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function3; } public final class io/modelcontextprotocol/kotlin/sdk/client/ClientKt { diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index 34b9a5bf3..49b20d7cb 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -4,6 +4,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.shared.Protocol import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions +import io.modelcontextprotocol.kotlin.sdk.shared.RequestHandlerExtra import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions import io.modelcontextprotocol.kotlin.sdk.shared.Transport import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema @@ -13,6 +14,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest import io.modelcontextprotocol.kotlin.sdk.types.CompleteResult +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams @@ -45,7 +47,9 @@ import io.modelcontextprotocol.kotlin.sdk.types.PingRequest import io.modelcontextprotocol.kotlin.sdk.types.PrimitiveSchemaDefinition import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Request import io.modelcontextprotocol.kotlin.sdk.types.RequestMeta +import io.modelcontextprotocol.kotlin.sdk.types.RequestResult import io.modelcontextprotocol.kotlin.sdk.types.Root import io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS @@ -323,6 +327,26 @@ public open class Client(private val clientInfo: Implementation, options: Client } } + /** + * Wraps incoming-request handlers with SEP-1577 client-side enforcement. + * + * For `sampling/createMessage`: if the incoming request carries `tools` or + * `toolChoice` but this client did not advertise [ClientCapabilities.Sampling.tools], + * the wrapper throws an [McpException] with JSON-RPC error code `InvalidParams` + * before the user-supplied handler runs. Matches the TypeScript SDK wrapper in + * `Client.setRequestHandler`. + */ + override fun wrapRequestHandler( + method: Method, + block: suspend (T, RequestHandlerExtra) -> RequestResult?, + ): suspend (T, RequestHandlerExtra) -> RequestResult? { + if (method != Method.Defined.SamplingCreateMessage) return block + return { request, extra -> + (request as? CreateMessageRequest)?.let { validateSamplingToolsCapability(it, capabilities) } + block(request, extra) + } + } + /** * Sends a ping request to the server to check connectivity. * diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt new file mode 100644 index 000000000..1290cb0fa --- /dev/null +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt @@ -0,0 +1,24 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.RPCError + +/** + * Validates SEP-1577 client-side enforcement: an incoming `sampling/createMessage` + * request that carries `tools` or `toolChoice` requires the client to have advertised + * the `sampling.tools` sub-capability. When the capability is missing, throws an + * [McpException] with JSON-RPC error code `InvalidParams`, matching TypeScript SDK + * client enforcement. + */ +internal fun validateSamplingToolsCapability(request: CreateMessageRequest, capabilities: ClientCapabilities) { + val params = request.params + if (params.tools == null && params.toolChoice == null) return + if (capabilities.sampling?.tools != null) return + val field = if (params.tools != null) "tools" else "toolChoice" + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Client does not support sampling with tools but request contains $field parameter", + ) +} diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt new file mode 100644 index 000000000..7775a08f3 --- /dev/null +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt @@ -0,0 +1,98 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.types.ToolChoice +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import kotlinx.serialization.json.buildJsonObject +import org.junit.jupiter.api.assertDoesNotThrow +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class ClientSamplingValidationTest { + + private val dummyTool = Tool( + name = "t", + inputSchema = ToolSchema(properties = buildJsonObject { }, required = emptyList()), + ) + + private val minimalMessages = listOf(SamplingMessage(Role.User, TextContent("hi"))) + + private val noToolsCaps = ClientCapabilities(sampling = ClientCapabilities.sampling) + private val withToolsCaps = ClientCapabilities( + sampling = ClientCapabilities.Sampling(tools = EmptyJsonObject), + ) + + @Test + fun `request without tools or toolChoice is always accepted`() { + val request = CreateMessageRequest( + CreateMessageRequestParams(maxTokens = 10, messages = minimalMessages), + ) + assertDoesNotThrow { validateSamplingToolsCapability(request, noToolsCaps) } + } + + @Test + fun `tools without sampling tools capability throws InvalidParams`() { + val request = CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 10, + messages = minimalMessages, + tools = listOf(dummyTool), + ), + ) + val exception = assertFailsWith { + validateSamplingToolsCapability(request, noToolsCaps) + } + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + check(exception.message?.contains("tools") == true) + } + + @Test + fun `toolChoice without sampling tools capability throws InvalidParams`() { + val request = CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 10, + messages = minimalMessages, + toolChoice = ToolChoice(mode = ToolChoice.Mode.Required), + ), + ) + val exception = assertFailsWith { + validateSamplingToolsCapability(request, noToolsCaps) + } + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + check(exception.message?.contains("toolChoice") == true) + } + + @Test + fun `tools with sampling tools capability is accepted`() { + val request = CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 10, + messages = minimalMessages, + tools = listOf(dummyTool), + ), + ) + assertDoesNotThrow { validateSamplingToolsCapability(request, withToolsCaps) } + } + + @Test + fun `toolChoice with sampling tools capability is accepted`() { + val request = CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 10, + messages = minimalMessages, + toolChoice = ToolChoice(mode = ToolChoice.Mode.Auto), + ), + ) + assertDoesNotThrow { validateSamplingToolsCapability(request, withToolsCaps) } + } +} diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index b6fe94228..c843aea06 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -79,6 +79,7 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { public final fun setFallbackRequestHandler (Lkotlin/jvm/functions/Function3;)V public final fun setNotificationHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function1;)V public final fun setRequestHandlerInternal (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function3;)V + protected fun wrapRequestHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function3; } public final class io/modelcontextprotocol/kotlin/sdk/shared/ProtocolKt { @@ -606,21 +607,23 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotificatio public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Companion; public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; public final fun component3 ()Lkotlinx/serialization/json/JsonObject; public final fun component4 ()Lkotlinx/serialization/json/JsonObject; public final fun component5 ()Ljava/util/Map; - public final fun copy (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; public fun equals (Ljava/lang/Object;)Z public final fun getElicitation ()Lkotlinx/serialization/json/JsonObject; public final fun getExperimental ()Lkotlinx/serialization/json/JsonObject; public final fun getExtensions ()Ljava/util/Map; public final fun getRoots ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; - public final fun getSampling ()Lkotlinx/serialization/json/JsonObject; + public final fun getSampling ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -638,7 +641,7 @@ public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapa public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Companion { public final fun getElicitation ()Lkotlinx/serialization/json/JsonObject; - public final fun getSampling ()Lkotlinx/serialization/json/JsonObject; + public final fun getSampling ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; public final fun serializer ()Lkotlinx/serialization/KSerializer; } @@ -671,6 +674,37 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$R public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; + public fun equals (Ljava/lang/Object;)Z + public final fun getContext ()Lkotlinx/serialization/json/JsonObject; + public final fun getTools ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; + 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/types/ClientCapabilities$Sampling;)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/types/ClientCapabilities$Sampling$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilitiesBuilder { public fun ()V public final fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; @@ -681,8 +715,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilitiesBu public final fun extensions (Ljava/util/Map;)V public final fun roots (Ljava/lang/Boolean;)V public static synthetic fun roots$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilitiesBuilder;Ljava/lang/Boolean;ILjava/lang/Object;)V - public final fun sampling (Lkotlin/jvm/functions/Function1;)V - public final fun sampling (Lkotlinx/serialization/json/JsonObject;)V + public final fun sampling (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;)V } public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ClientNotification : io/modelcontextprotocol/kotlin/sdk/types/Notification { @@ -934,6 +967,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ContentTypes : java/ public static final field IMAGE Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; public static final field RESOURCE_LINK Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; public static final field TEXT Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public static final field TOOL_RESULT Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public static final field TOOL_USE Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; public static fun getEntries ()Lkotlin/enums/EnumEntries; public final fun getValue ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; @@ -1006,9 +1041,11 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams$Companion; - public synthetic fun (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()I + public final fun component10 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice; + public final fun component11-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; public final fun component2 ()Ljava/util/List; public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences; public final fun component4 ()Ljava/lang/String; @@ -1016,9 +1053,9 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest public final fun component6 ()Ljava/lang/Double; public final fun component7 ()Ljava/util/List; public final fun component8 ()Lkotlinx/serialization/json/JsonObject; - public final fun component9-VI-3G7E ()Lkotlinx/serialization/json/JsonObject; - public final fun copy-qtvSJzw (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams; - public static synthetic fun copy-qtvSJzw$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams; + public final fun component9 ()Ljava/util/List; + public final fun copy-APmi5RE (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams; + public static synthetic fun copy-APmi5RE$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams; public fun equals (Ljava/lang/Object;)Z public final fun getIncludeContext ()Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext; public final fun getMaxTokens ()I @@ -1029,6 +1066,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest public final fun getStopSequences ()Ljava/util/List; public final fun getSystemPrompt ()Ljava/lang/String; public final fun getTemperature ()Ljava/lang/Double; + public final fun getToolChoice ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice; + public final fun getTools ()Ljava/util/List; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1050,17 +1089,19 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult$Companion; - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; + public final fun component2 ()Ljava/util/List; public final fun component3 ()Ljava/lang/String; public final fun component4-6olV-UY ()Ljava/lang/String; public final fun component5 ()Lkotlinx/serialization/json/JsonObject; - public final fun copy-ctVX1tw (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult; - public static synthetic fun copy-ctVX1tw$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult; + public final fun copy-ctVX1tw (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult; + public static synthetic fun copy-ctVX1tw$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult; public fun equals (Ljava/lang/Object;)Z - public final fun getContent ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; + public final fun getContent ()Ljava/util/List; public fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getModel ()Ljava/lang/String; public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; @@ -2937,7 +2978,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/McpException : java/ public final fun getData ()Lkotlinx/serialization/json/JsonElement; } -public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/MediaContent : io/modelcontextprotocol/kotlin/sdk/types/ContentBlock { +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/MediaContent : io/modelcontextprotocol/kotlin/sdk/types/ContentBlock, io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent$Companion; } @@ -4199,13 +4240,18 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Roots_dslKt { public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessage { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage$Companion; - public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; - public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage; + public final fun component2 ()Ljava/util/List; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage; public fun equals (Ljava/lang/Object;)Z - public final fun getContent ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent; + public final fun getContent ()Ljava/util/List; + public final fun getMeta ()Lkotlinx/serialization/json/JsonObject; public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/types/Role; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -4233,6 +4279,15 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuild public final fun user (Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)V } +public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent : io/modelcontextprotocol/kotlin/sdk/types/WithMeta { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent$Companion; + public abstract fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/Sampling_dslKt { public static final fun assistant (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function0;)V public static final fun assistantAudio (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V @@ -4508,6 +4563,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/StopReason$Companion public final fun getEndTurn--MeDoko ()Ljava/lang/String; public final fun getMaxTokens--MeDoko ()Ljava/lang/String; public final fun getStopSequence--MeDoko ()Ljava/lang/String; + public final fun getToolUse--MeDoko ()Ljava/lang/String; public final fun serializer ()Lkotlinx/serialization/KSerializer; } @@ -5077,6 +5133,49 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations$Comp public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice; + public fun equals (Ljava/lang/Object;)Z + public final fun getMode ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice; + 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/types/ToolChoice;)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/types/ToolChoice$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode : java/lang/Enum { + public static final field Auto Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode$Companion; + public static final field None Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; + public static final field Required Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; + public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ToolExecution { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution$Companion; public fun ()V @@ -5137,6 +5236,43 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotif public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolResultContent : io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent$Companion; + public fun (Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/List; + public final fun component3 ()Lkotlinx/serialization/json/JsonObject; + public final fun component4 ()Ljava/lang/Boolean; + public final fun component5 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent;Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent; + public fun equals (Ljava/lang/Object;)Z + public final fun getContent ()Ljava/util/List; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject; + public final fun getToolUseId ()Ljava/lang/String; + public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public fun hashCode ()I + public final fun isError ()Ljava/lang/Boolean; + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolResultContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent; + 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/types/ToolResultContent;)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/types/ToolResultContent$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ToolSchema { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema$Companion; public fun ()V @@ -5173,6 +5309,41 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolSchema$Companion public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ToolUseContent : io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + 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 (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent; + public fun equals (Ljava/lang/Object;)Z + public final fun getId ()Ljava/lang/String; + public final fun getInput ()Lkotlinx/serialization/json/JsonObject; + public fun getMeta ()Lkotlinx/serialization/json/JsonObject; + public final fun getName ()Ljava/lang/String; + public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolUseContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent; + 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/types/ToolUseContent;)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/types/ToolUseContent$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ToolsKt { public static final fun error (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; public static synthetic fun error$default (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult; diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index bc3cf7673..75cebd3dd 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -556,16 +556,31 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio block: suspend (T, RequestHandlerExtra) -> RequestResult?, ) { assertRequestHandlerCapability(method) + val wrapped = wrapRequestHandler(method, block) _requestHandlers.update { current -> current.put(method.value) { jSONRPCRequest, extraHandler -> val request = jSONRPCRequest.fromJSON() - val response = block(request as T, extraHandler) + val response = wrapped(request as T, extraHandler) response } } } + /** + * Subclass hook to wrap an incoming-request handler before it is registered. + * + * Called once by [setRequestHandler] during registration. Subclasses may return a + * new function that, when invoked, performs additional checks (capability gates, + * schema validation, etc.) before delegating to [block]. The default implementation + * is the identity. + */ + @Suppress("UNUSED_PARAMETER") + protected open fun wrapRequestHandler( + method: Method, + block: suspend (T, RequestHandlerExtra) -> RequestResult?, + ): suspend (T, RequestHandlerExtra) -> RequestResult? = block + /** * Removes the request handler for the given method. */ diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt index d936381d9..75e923b2d 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt @@ -21,7 +21,7 @@ import kotlinx.serialization.json.buildJsonObject * val request = buildInitializeRequest { * protocolVersion = "1.0" * capabilities { - * sampling(ClientCapabilities.sampling) + * sampling(ClientCapabilities.Sampling()) * roots(listChanged = true) * experimental { * put("customFeature", JsonPrimitive(true)) @@ -36,46 +36,32 @@ import kotlinx.serialization.json.buildJsonObject */ @McpDsl public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { - private var sampling: JsonObject? = null + private var sampling: ClientCapabilities.Sampling? = null private var roots: ClientCapabilities.Roots? = null private var elicitation: JsonObject? = null private var extensions: Map? = null private var experimental: JsonObject? = null /** - * Indicates that the client supports sampling from an LLM. + * Sampling capability configuration. See [ClientCapabilities.Sampling]. * - * Use [ClientCapabilities.sampling] for default empty configuration. + * Pass `ClientCapabilities.Sampling()` to enable base sampling with no sub-capabilities. + * Construct `ClientCapabilities.Sampling(tools = EmptyJsonObject, context = EmptyJsonObject)` + * directly to enable SEP-1577 sub-capabilities. * * Example: * ```kotlin * capabilities { - * sampling(ClientCapabilities.sampling) + * sampling(ClientCapabilities.Sampling(tools = EmptyJsonObject)) * } * ``` * * @param value The sampling capability configuration */ - public fun sampling(value: JsonObject) { + public fun sampling(value: ClientCapabilities.Sampling) { this.sampling = value } - /** - * Indicates that the client supports sampling from an LLM with custom configuration. - * - * Example: - * ```kotlin - * capabilities { - * sampling { - * put("temperature", JsonPrimitive(0.7)) - * } - * } - * ``` - * - * @param block Lambda for building the sampling configuration - */ - public fun sampling(block: JsonObjectBuilder.() -> Unit): Unit = sampling(buildJsonObject(block)) - /** * Indicates that the client supports listing roots. * diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt index 14248fae7..e13a75036 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt @@ -41,6 +41,7 @@ public data class Implementation( * supports that capability. * * @property sampling Present if the client supports sampling from an LLM. + * Use [ClientCapabilities.Sampling] to configure SEP-1577 sub-capabilities (tools, context). * @property roots Present if the client supports listing roots. * @property elicitation Present if the client supports elicitation from the server. * @property experimental Experimental, non-standard capabilities that the client supports. @@ -51,7 +52,7 @@ public data class Implementation( */ @Serializable public data class ClientCapabilities( - public val sampling: JsonObject? = null, + public val sampling: Sampling? = null, public val roots: Roots? = null, public val elicitation: JsonObject? = null, public val experimental: JsonObject? = null, @@ -59,11 +60,51 @@ public data class ClientCapabilities( ) { /** - * @property sampling convenience value to enable the sampling capability + * Source-compatibility constructor retaining the pre-SEP-1577 `sampling: JsonObject?` + * shape. Any non-null [sampling] is converted to an empty [Sampling] (sub-capabilities + * cannot be recovered from the old opaque `JsonObject`). + */ + @Deprecated( + "ClientCapabilities.sampling is now typed. Pass a ClientCapabilities.Sampling? " + + "instead of JsonObject?.", + ReplaceWith( + "ClientCapabilities(sampling?.let { ClientCapabilities.Sampling() }, " + + "roots, elicitation, experimental, extensions)", + ), + ) + public constructor( + sampling: JsonObject?, + roots: Roots? = null, + elicitation: JsonObject? = null, + experimental: JsonObject? = null, + extensions: Map? = null, + ) : this( + sampling = sampling?.let { Sampling() }, + roots = roots, + elicitation = elicitation, + experimental = experimental, + extensions = extensions, + ) + + /** + * sub-capabilities for sampling. + * + * @property context Present if the client supports context inclusion via + * [CreateMessageRequestParams.includeContext] with values other than [IncludeContext.None]. + * Servers SHOULD avoid non-`none` values when this field is absent. + * @property tools Present if the client supports tool use via + * [CreateMessageRequestParams.tools] / [CreateMessageRequestParams.toolChoice]. + * Servers MUST NOT send `tools`/`toolChoice` when this field is absent. + */ + @Serializable + public data class Sampling(public val context: JsonObject? = null, public val tools: JsonObject? = null) + + /** + * @property sampling convenience value to enable the base sampling capability with no sub-capabilities * @property elicitation convenience value to enable the elicitation capability */ public companion object { - public val sampling: JsonObject = EmptyJsonObject + public val sampling: Sampling = Sampling() public val elicitation: JsonObject = EmptyJsonObject } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt index a1c9e4fec..d30c22e40 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt @@ -29,6 +29,12 @@ public enum class ContentTypes(public val value: String) { @SerialName("resource") EMBEDDED_RESOURCE("resource"), + + @SerialName("tool_use") + TOOL_USE("tool_use"), + + @SerialName("tool_result") + TOOL_RESULT("tool_result"), } /** @@ -43,9 +49,27 @@ public sealed interface ContentBlock : WithMeta { /** * Content block that carries media data such as text, images, or audio. + * + * Every [MediaContent] is also a valid [SamplingMessageContent]; the sampling content + * hierarchy additionally admits [ToolUseContent] and [ToolResultContent]. */ @Serializable(with = MediaContentPolymorphicSerializer::class) -public sealed interface MediaContent : ContentBlock +public sealed interface MediaContent : + ContentBlock, + SamplingMessageContent + +/** + * Content block that can appear inside a [SamplingMessage] or [CreateMessageResult]. + * + * Implemented by [TextContent], [ImageContent], [AudioContent], [ToolUseContent], + * and [ToolResultContent]. + * + * @property type discriminator identifying the content block subtype + */ +@Serializable(with = SamplingMessageContentPolymorphicSerializer::class) +public sealed interface SamplingMessageContent : WithMeta { + public val type: ContentTypes +} /** * Text provided to or from an LLM. @@ -174,3 +198,50 @@ public data class EmbeddedResource( @EncodeDefault public override val type: ContentTypes = ContentTypes.EMBEDDED_RESOURCE } + +/** + * A request from the assistant to invoke a tool during sampling. + * + * @property id Unique identifier for this tool use; matches a subsequent + * [ToolResultContent.toolUseId] that reports the result. + * @property name The tool name (must match a tool declared in the sampling request's tools list). + * @property input The arguments to pass to the tool, conforming to the tool's input schema. + * @property meta property/parameter is reserved by MCP to allow clients and servers + * to attach additional metadata to their interactions. + */ +@Serializable +public data class ToolUseContent( + val id: String, + val name: String, + val input: JsonObject, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : SamplingMessageContent { + @EncodeDefault + public override val type: ContentTypes = ContentTypes.TOOL_USE +} + +/** + * The result of a tool call previously requested via [ToolUseContent], supplied back + * to the assistant on the next sampling turn. + * + * @property toolUseId The id of the [ToolUseContent] this result corresponds to. + * @property content The unstructured result, following the same shape as [CallToolResult.content]. + * @property structuredContent Optional structured result; if the tool declared an output schema, + * this SHOULD conform to it. + * @property isError Whether the tool call ended in error. Defaults to absent (treated as false). + * @property meta property/parameter is reserved by MCP to allow clients and servers + * to attach additional metadata to their interactions. + */ +@Serializable +public data class ToolResultContent( + val toolUseId: String, + val content: List = emptyList(), + val structuredContent: JsonObject? = null, + val isError: Boolean? = null, + @SerialName("_meta") + override val meta: JsonObject? = null, +) : SamplingMessageContent { + @EncodeDefault + public override val type: ContentTypes = ContentTypes.TOOL_RESULT +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt index 2cb58b77b..f3e1575bc 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt @@ -21,7 +21,7 @@ import kotlin.contracts.contract * val request = buildInitializeRequest { * protocolVersion = "2024-11-05" * capabilities { - * sampling(ClientCapabilities.sampling) + * sampling(ClientCapabilities.Sampling()) * roots(listChanged = true) * } * info("MyClient", "1.0.0") @@ -33,7 +33,7 @@ import kotlin.contracts.contract * val request = buildInitializeRequest { * protocolVersion = "2024-11-05" * capabilities { - * sampling(ClientCapabilities.sampling) + * sampling(ClientCapabilities.Sampling()) * experimental { * put("feature", JsonPrimitive(true)) * } @@ -94,7 +94,7 @@ public class InitializeRequestBuilder @PublishedApi internal constructor() : Req * Example: * ```kotlin * capabilities(ClientCapabilities( - * sampling = ClientCapabilities.sampling, + * sampling = ClientCapabilities.Sampling(), * roots = ClientCapabilities.Roots(listChanged = true) * )) * ``` @@ -113,7 +113,7 @@ public class InitializeRequestBuilder @PublishedApi internal constructor() : Req * Example: * ```kotlin * capabilities { - * sampling(ClientCapabilities.sampling) + * sampling(ClientCapabilities.Sampling()) * roots(listChanged = true) * elicitation(ClientCapabilities.elicitation) * } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt index 1bc00e281..4752ff50b 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt @@ -112,8 +112,8 @@ public class CreateMessageRequestBuilder @PublishedApi internal constructor() : * Example: * ```kotlin * messages(listOf( - * SamplingMessage(Role.User, TextContent("Hello")), - * SamplingMessage(Role.Assistant, TextContent("Hi!")) + * SamplingMessage(Role.User, listOf(TextContent("Hello"))), + * SamplingMessage(Role.Assistant, listOf(TextContent("Hi!"))) * )) * ``` */ @@ -246,7 +246,7 @@ public class SamplingMessageBuilder @PublishedApi internal constructor() { * ``` */ public fun user(content: MediaContent) { - messages.add(SamplingMessage(Role.User, content)) + messages.add(SamplingMessage(Role.User, listOf(content))) } /** @@ -258,7 +258,7 @@ public class SamplingMessageBuilder @PublishedApi internal constructor() { * ``` */ public fun assistant(content: MediaContent) { - messages.add(SamplingMessage(Role.Assistant, content)) + messages.add(SamplingMessage(Role.Assistant, listOf(content))) } @PublishedApi diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt index 97fc0c06c..211fdc798 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt @@ -75,11 +75,33 @@ public data class ModelPreferences( * * Used in sampling requests to provide conversation context and history to the LLM. * + * Under SEP-1577, [content] is a list of [SamplingMessageContent] blocks rather than a + * single block. On the wire, a list of size 1 is serialised as a single object (wire-compatible + * with pre-SEP single-block content); a list of size 2+ is serialised as a JSON array. + * * @property role The role of the message sender (user, assistant, or system). - * @property content The content of the message. Can be text, image, or audio content. + * @property content The content blocks of the message; MUST contain at least one block. + * @property meta Optional metadata reserved by MCP for clients and servers. */ @Serializable -public data class SamplingMessage(val role: Role, val content: MediaContent) +public data class SamplingMessage( + val role: Role, + @Serializable(with = SamplingContentSerializer::class) + val content: List, + @SerialName("_meta") + val meta: JsonObject? = null, +) { + /** + * Convenience constructor for a single-block message. Wraps [content] in a + * singleton list so call sites can write `SamplingMessage(Role.User, TextContent("hi"))` + * without the explicit `listOf(...)`. + */ + public constructor( + role: Role, + content: SamplingMessageContent, + meta: JsonObject? = null, + ) : this(role, listOf(content), meta) +} // ============================================================================ // sampling/createMessage @@ -184,6 +206,12 @@ public data class CreateMessageRequest(override val params: CreateMessageRequest * @property stopSequences Optional list of sequences that will stop generation if encountered. * @property metadata Optional metadata to pass through to the LLM provider. * The format of this metadata is provider-specific. + * @property tools Optional list of tools the model may use during generation. + * The client MUST return an error if this field is present but the client did not advertise + * [ClientCapabilities.Sampling.tools]. + * @property toolChoice Optional policy controlling how the model uses the provided [tools]. + * The client MUST return an error if this field is present but the client did not advertise + * [ClientCapabilities.Sampling.tools]. * @property meta Optional metadata for this request. May include a progressToken for * out-of-band progress notifications. */ @@ -197,6 +225,8 @@ public data class CreateMessageRequestParams( val temperature: Double? = null, val stopSequences: List? = null, val metadata: JsonObject? = null, + val tools: List? = null, + val toolChoice: ToolChoice? = null, @SerialName("_meta") override val meta: RequestMeta? = null, ) : RequestParams @@ -226,22 +256,37 @@ public enum class IncludeContext { * to inspect the response (human in the loop) and decide whether to allow the server to see it. * * @property role The role of the message sender. Typically [Role.Assistant] for LLM-generated responses. - * @property content The generated content. Can be text, image, or audio content. - * @property model The name of the model that generated the message (e.g., "claude-3-opus-20240229", - * "gpt-4-turbo-preview"). This helps the server understand which model was used. + * @property content The generated content blocks; at least one block is required. + * @property model The name of the model that generated the message (e.g., "claude-3-opus-20240229"). * @property stopReason The reason why sampling stopped, if known. - * Common values: "end_turn", "max_tokens", "stop_sequence", "content_filter" + * Common values: [StopReason.EndTurn], [StopReason.StopSequence], [StopReason.MaxTokens], + * [StopReason.ToolUse]. * @property meta Optional metadata for this response. */ @Serializable public data class CreateMessageResult( val role: Role, - val content: MediaContent, + @Serializable(with = SamplingContentSerializer::class) + val content: List, val model: String, val stopReason: StopReason? = null, @SerialName("_meta") override val meta: JsonObject? = null, -) : ClientResult +) : ClientResult { + /** + * Convenience constructor for a single-block response. Wraps [content] in a + * singleton list so call sites can write + * `CreateMessageResult(Role.Assistant, TextContent("ok"), "model-name")` + * without the explicit `listOf(...)`. + */ + public constructor( + role: Role, + content: SamplingMessageContent, + model: String, + stopReason: StopReason? = null, + meta: JsonObject? = null, + ) : this(role, listOf(content), model, stopReason, meta) +} /** * The reason why the LLM stopped generating tokens. @@ -255,10 +300,38 @@ public value class StopReason(public val value: String) { * @property EndTurn generation ended naturally * @property StopSequence a stop sequence was encountered * @property MaxTokens the maximum token limit was reached + * @property ToolUse the assistant issued a tool call (see SEP-1577) */ public companion object { public val EndTurn: StopReason = StopReason("endTurn") public val StopSequence: StopReason = StopReason("stopSequence") public val MaxTokens: StopReason = StopReason("maxTokens") + public val ToolUse: StopReason = StopReason("toolUse") + } +} + +/** + * Controls tool-selection behaviour for [CreateMessageRequest] under SEP-1577. + * + * @property mode Tool-use policy: + * - [Mode.Auto]: the model decides whether to use tools (default when [mode] is absent) + * - [Mode.Required]: the model MUST use at least one tool before completing + * - [Mode.None]: the model MUST NOT use any tool + */ +@Serializable +public data class ToolChoice(public val mode: Mode? = null) { + /** + * Allowed values of [ToolChoice.mode] per SEP-1577. + */ + @Serializable + public enum class Mode { + @SerialName("auto") + Auto, + + @SerialName("required") + Required, + + @SerialName("none") + None, } } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt index 3e259575b..4a1abc61b 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt @@ -4,13 +4,18 @@ import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject @@ -121,6 +126,101 @@ internal object MediaContentPolymorphicSerializer : } } +/** + * Polymorphic serializer for [SamplingMessageContent] types. + * Determines the concrete type based on the "type" field in JSON. + */ +internal object SamplingMessageContentPolymorphicSerializer : + JsonContentPolymorphicSerializer(SamplingMessageContent::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + when (element.getType()) { + ContentTypes.TEXT.value -> TextContent.serializer() + + ContentTypes.IMAGE.value -> ImageContent.serializer() + + ContentTypes.AUDIO.value -> AudioContent.serializer() + + ContentTypes.TOOL_USE.value -> ToolUseContent.serializer() + + ContentTypes.TOOL_RESULT.value -> ToolResultContent.serializer() + + else -> throw SerializationException( + "Unknown sampling message content type: ${element.getTypeOrNull()}", + ) + } +} + +/** + * Wire-format serializer for `List` honouring SEP-1577's + * single-object-or-array content shape. + * + * **Why this exists.** SEP-1577 widens the `content` field of [SamplingMessage] and + * [CreateMessageResult] from a single content block to either a single block or an + * array of blocks. Pre-SEP peers only understand a single object on the wire; post-SEP + * peers may send arrays when a turn carries multiple blocks (e.g. an assistant reply + * containing `[TextContent, ToolUseContent]` during a tool-loop step). + * + * The Kotlin API uses `List` uniformly to avoid branching in + * caller code. This serializer bridges the type difference: + * + * - **Read:** accepts both shapes; a single JSON object becomes `listOf(block)`, an + * array becomes the decoded list. + * - **Write:** size-1 list → single object (wire-compatible with pre-SEP peers); + * size≥2 → array; empty list throws (sampling content has no valid empty meaning + * per spec). + */ +internal object SamplingContentSerializer : KSerializer> { + + private val listSerializer = ListSerializer(SamplingMessageContentPolymorphicSerializer) + + override val descriptor: SerialDescriptor = listSerializer.descriptor + + override fun serialize(encoder: Encoder, value: List) { + check(value.isNotEmpty()) { "content must contain at least one block" } + val jsonEncoder = encoder as? JsonEncoder + ?: throw SerializationException("SamplingContentSerializer requires a Json encoder") + if (value.size == 1) { + jsonEncoder.encodeJsonElement( + jsonEncoder.json.encodeToJsonElement( + SamplingMessageContentPolymorphicSerializer, + value[0], + ), + ) + } else { + jsonEncoder.encodeJsonElement( + jsonEncoder.json.encodeToJsonElement(listSerializer, value), + ) + } + } + + override fun deserialize(decoder: Decoder): List { + val jsonDecoder = decoder as? JsonDecoder + ?: throw SerializationException("SamplingContentSerializer requires a Json decoder") + return decodeFromElement(jsonDecoder, jsonDecoder.decodeJsonElement()) + } + + private fun decodeFromElement(jsonDecoder: JsonDecoder, element: JsonElement): List = + when (element) { + is JsonArray -> { + if (element.isEmpty()) { + throw SerializationException("content must contain at least one block") + } + jsonDecoder.json.decodeFromJsonElement(listSerializer, element) + } + + is JsonObject -> listOf( + jsonDecoder.json.decodeFromJsonElement( + SamplingMessageContentPolymorphicSerializer, + element, + ), + ) + + else -> throw SerializationException( + "content must be a JSON object or array of objects, got $element", + ) + } +} + // ============================================================================ // Resource Serializers // ============================================================================ diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt index 171c249dc..8b2e4f849 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import io.modelcontextprotocol.kotlin.test.utils.verifySerializationRoundTrip @@ -8,6 +9,7 @@ import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull import kotlin.test.assertNull class CapabilitiesTest { @@ -119,7 +121,7 @@ class CapabilitiesTest { @Test fun `should serialize ClientCapabilities with sampling`() { val capabilities = ClientCapabilities( - sampling = ClientCapabilities.sampling, + sampling = ClientCapabilities.Sampling(), ) verifySerialization( capabilities, @@ -266,7 +268,7 @@ class CapabilitiesTest { "io.modelcontextprotocol/ui" to EmptyJsonObject, ) val capabilities = ClientCapabilities( - sampling = ClientCapabilities.sampling, + sampling = ClientCapabilities.Sampling(), roots = ClientCapabilities.Roots(listChanged = true), elicitation = ClientCapabilities.elicitation, experimental = experimental, @@ -309,7 +311,7 @@ class CapabilitiesTest { val capabilities = verifyDeserialization(McpJson, json) - assertEquals(EmptyJsonObject, capabilities.sampling) + assertEquals(ClientCapabilities.Sampling(), capabilities.sampling) assertEquals(true, capabilities.roots?.listChanged) assertEquals(EmptyJsonObject, capabilities.elicitation) } @@ -330,7 +332,7 @@ class CapabilitiesTest { @Test fun `should serialize and deserialize ClientCapabilities round trip`() { val original = ClientCapabilities( - sampling = ClientCapabilities.sampling, + sampling = ClientCapabilities.Sampling(), roots = ClientCapabilities.Roots(listChanged = false), elicitation = ClientCapabilities.elicitation, extensions = mapOf( @@ -734,10 +736,11 @@ class CapabilitiesTest { } """.trimIndent() - val capabilities = verifyDeserialization(McpJson, json) + // Sampling is now a typed struct; unknown fields are ignored on deserialization + val capabilities = McpJson.decodeFromString(json) - // Should not fail - additionalProperties are allowed - assertEquals("customValue", capabilities.sampling?.get("customProperty")?.toString()?.trim('"')) + // Should not fail - additionalProperties are allowed (unknown fields are ignored by Sampling) + assertNotNull(capabilities.sampling) } @Test @@ -755,4 +758,50 @@ class CapabilitiesTest { // Should not fail - additionalProperties are allowed assertEquals("debug", capabilities.logging?.get("level")?.toString()?.trim('"')) } + + // ============================================================================ + // ClientCapabilities.Sampling sub-capabilities (SEP-1577) + // ============================================================================ + + @Test + fun `empty sampling serialises as empty object`() { + val caps = ClientCapabilities(sampling = ClientCapabilities.Sampling()) + val json = McpJson.encodeToString(ClientCapabilities.serializer(), caps) + check("\"sampling\":{}" in json) { json } + } + + @Test + fun `sampling with tools sub-capability serialises tools`() { + val caps = ClientCapabilities(sampling = ClientCapabilities.Sampling(tools = EmptyJsonObject)) + val json = McpJson.encodeToString(ClientCapabilities.serializer(), caps) + check("\"tools\":{}" in json) { json } + } + + @Test + fun `sampling with context sub-capability serialises context`() { + val caps = ClientCapabilities(sampling = ClientCapabilities.Sampling(context = EmptyJsonObject)) + val json = McpJson.encodeToString(ClientCapabilities.serializer(), caps) + check("\"context\":{}" in json) { json } + } + + @Test + fun `sampling with combined tools and context round-trips`() { + val original = ClientCapabilities( + sampling = ClientCapabilities.Sampling( + context = EmptyJsonObject, + tools = EmptyJsonObject, + ), + ) + val json = McpJson.encodeToString(ClientCapabilities.serializer(), original) + val decoded = McpJson.decodeFromString(ClientCapabilities.serializer(), json) + decoded shouldBe original + decoded.sampling?.tools shouldBe EmptyJsonObject + decoded.sampling?.context shouldBe EmptyJsonObject + } + + @Test + fun `absent sampling means unsupported`() { + val caps = ClientCapabilities() + caps.sampling shouldBe null + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt index f110326fb..c6dee5dca 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt @@ -1,17 +1,26 @@ package io.modelcontextprotocol.kotlin.sdk.types +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertIs import kotlin.test.assertNull class ContentTest { + private val samplingContentSerializer = ListSerializer(SamplingMessageContent.serializer()) + @Test fun `should serialize TextContent with minimal fields`() { val content = TextContent(text = "Hello, MCP!") @@ -308,4 +317,133 @@ class ContentTest { assertIs(content[1]) assertIs(content[2]) } + + // ============================================================================ + // SamplingMessageContent hierarchy (SEP-1577) + // ============================================================================ + + @Test + fun `TextContent implements SamplingMessageContent`() { + val c: SamplingMessageContent = TextContent("hi") + c.shouldBeInstanceOf() + } + + @Test + fun `ImageContent implements SamplingMessageContent`() { + val c: SamplingMessageContent = ImageContent(data = "AA==", mimeType = "image/png") + c.shouldBeInstanceOf() + } + + @Test + fun `AudioContent implements SamplingMessageContent`() { + val c: SamplingMessageContent = AudioContent(data = "AA==", mimeType = "audio/wav") + c.shouldBeInstanceOf() + } + + // ============================================================================ + // ToolUseContent + // ============================================================================ + + @Test + fun `ToolUseContent round-trips through McpJson`() { + val input = buildJsonObject { put("location", JsonPrimitive("London")) } + val original = ToolUseContent(id = "call_1", name = "get_weather", input = input) + val encoded = McpJson.encodeToString(ToolUseContent.serializer(), original) + val decoded = McpJson.decodeFromString(ToolUseContent.serializer(), encoded) + decoded shouldBe original + } + + @Test + fun `ToolUseContent serialises with type discriminator tool_use`() { + val c = ToolUseContent(id = "x", name = "n", input = JsonObject(emptyMap())) + val json = McpJson.encodeToString(ToolUseContent.serializer(), c) + check("\"type\":\"tool_use\"" in json) { "missing discriminator in: $json" } + } + + @Test + fun `ToolUseContent preserves meta when present`() { + val meta = buildJsonObject { put("cacheControl", JsonPrimitive("ephemeral")) } + val c = ToolUseContent(id = "x", name = "n", input = JsonObject(emptyMap()), meta = meta) + val json = McpJson.encodeToString(ToolUseContent.serializer(), c) + val decoded = McpJson.decodeFromString(ToolUseContent.serializer(), json) + decoded.meta shouldBe meta + } + + // ============================================================================ + // ToolResultContent + // ============================================================================ + + @Test + fun `ToolResultContent round-trips with text content block`() { + val original = ToolResultContent( + toolUseId = "call_1", + content = listOf(TextContent("20°C sunny")), + ) + val encoded = McpJson.encodeToString(ToolResultContent.serializer(), original) + val decoded = McpJson.decodeFromString(ToolResultContent.serializer(), encoded) + decoded shouldBe original + } + + @Test + fun `ToolResultContent round-trips with mixed content including resource_link`() { + val original = ToolResultContent( + toolUseId = "call_2", + content = listOf( + TextContent("see file"), + ResourceLink(name = "log", uri = "file:///tmp/a.log"), + ), + ) + val encoded = McpJson.encodeToString(ToolResultContent.serializer(), original) + val decoded = McpJson.decodeFromString(ToolResultContent.serializer(), encoded) + decoded shouldBe original + } + + @Test + fun `ToolResultContent serialises with discriminator and preserves structuredContent and isError`() { + val original = ToolResultContent( + toolUseId = "call_3", + content = emptyList(), + structuredContent = buildJsonObject { put("temp", JsonPrimitive(20)) }, + isError = true, + ) + val json = McpJson.encodeToString(ToolResultContent.serializer(), original) + check("\"type\":\"tool_result\"" in json) { "missing discriminator in: $json" } + val decoded = McpJson.decodeFromString(ToolResultContent.serializer(), json) + decoded shouldBe original + } + + // ============================================================================ + // SamplingMessageContentPolymorphicSerializer + // ============================================================================ + + @Test + fun `SamplingMessageContent polymorphic serializer decodes text`() { + val json = """[{"type":"text","text":"hi"}]""" + val list = McpJson.decodeFromString(samplingContentSerializer, json) + list[0].shouldBeInstanceOf().text shouldBe "hi" + } + + @Test + fun `SamplingMessageContent polymorphic serializer decodes tool_use`() { + val json = """[{"type":"tool_use","id":"a","name":"n","input":{}}]""" + val list = McpJson.decodeFromString(samplingContentSerializer, json) + val use = list[0].shouldBeInstanceOf() + use.id shouldBe "a" + use.name shouldBe "n" + } + + @Test + fun `SamplingMessageContent polymorphic serializer decodes tool_result`() { + val json = """[{"type":"tool_result","toolUseId":"a","content":[]}]""" + val list = McpJson.decodeFromString(samplingContentSerializer, json) + list[0].shouldBeInstanceOf().toolUseId shouldBe "a" + } + + @Test + fun `SamplingMessageContent polymorphic serializer throws on unknown discriminator`() { + val json = """[{"type":"bogus","x":1}]""" + assertFailsWith { + McpJson.decodeFromString(samplingContentSerializer, json) + } + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt index 3da2a8bd9..ed97c18fd 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt @@ -21,7 +21,7 @@ class InitializeTest { InitializeRequestParams( protocolVersion = "2024-11-05", capabilities = ClientCapabilities( - sampling = ClientCapabilities.sampling, + sampling = ClientCapabilities.Sampling(), roots = ClientCapabilities.Roots(listChanged = true), elicitation = ClientCapabilities.elicitation, experimental = buildJsonObject { diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt index 696f32216..0432abac1 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt @@ -1,7 +1,13 @@ package io.modelcontextprotocol.kotlin.sdk.types +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.double import kotlinx.serialization.json.jsonPrimitive @@ -14,6 +20,21 @@ import kotlin.test.assertNotNull class SamplingTest { + @Serializable + private data class Holder( + @Serializable(with = SamplingContentSerializer::class) + val content: List, + ) + + private val dummyTool = Tool( + name = "get_weather", + description = "returns weather", + inputSchema = ToolSchema( + properties = buildJsonObject { }, + required = emptyList(), + ), + ) + @Test fun `should serialize ModelHint`() { val hint = ModelHint(name = "claude-3-5-sonnet") @@ -66,7 +87,7 @@ class SamplingTest { fun `should serialize SamplingMessage`() { val message = SamplingMessage( role = Role.User, - content = TextContent(text = "Summarize the latest release."), + content = listOf(TextContent(text = "Summarize the latest release.")), ) verifySerialization( @@ -93,11 +114,11 @@ class SamplingTest { messages = listOf( SamplingMessage( role = Role.User, - content = TextContent(text = "You are a helpful assistant."), + content = listOf(TextContent(text = "You are a helpful assistant.")), ), SamplingMessage( role = Role.User, - content = TextContent(text = "Provide a short summary."), + content = listOf(TextContent(text = "Provide a short summary.")), ), ), modelPreferences = ModelPreferences( @@ -202,7 +223,7 @@ class SamplingTest { val message = params.messages.first() assertEquals(Role.User, message.role) - val content = assertIs(message.content) + val content = assertIs(message.content.single()) assertEquals("Draft a project update.", content.text) val preferences = assertNotNull(params.modelPreferences) @@ -259,7 +280,7 @@ class SamplingTest { val result = verifyDeserialization(McpJson, json) assertEquals(Role.Assistant, result.role) - val text = assertIs(result.content) + val text = assertIs(result.content.single()) assertEquals("Summary complete.", text.text) assertEquals("gpt-4o", result.model) assertEquals(StopReason.StopSequence, result.stopReason) @@ -267,4 +288,211 @@ class SamplingTest { assertNotNull(meta) assertEquals(1200.5, meta["latencyMs"]?.jsonPrimitive?.double) } + + // ============================================================================ + // SamplingMessage shape (SEP-1577) + // ============================================================================ + + @Test + fun `SamplingMessage content is a list of SamplingMessageContent`() { + val m = SamplingMessage( + role = Role.User, + content = listOf(TextContent("hi")), + ) + m.content.size shouldBe 1 + (m.content[0] as TextContent).text shouldBe "hi" + } + + @Test + fun `SamplingMessage single-element content serialises as single object`() { + val m = SamplingMessage(role = Role.User, content = listOf(TextContent("hi"))) + val json = McpJson.encodeToString(SamplingMessage.serializer(), m) + check("""{"role":"user","content":""" in json) { "expected role/content prefix, got $json" } + check("\"type\":\"text\"" in json && "\"text\":\"hi\"" in json) { + "expected single-object content with text discriminator, got $json" + } + check("\"content\":[" !in json) { "expected single-object wire, but array form found: $json" } + } + + @Test + fun `SamplingMessage multi-element content serialises as array`() { + val m = SamplingMessage( + role = Role.Assistant, + content = listOf( + TextContent("Let me use a tool"), + ToolUseContent(id = "c1", name = "get_weather", input = JsonObject(emptyMap())), + ), + ) + val json = McpJson.encodeToString(SamplingMessage.serializer(), m) + check("\"content\":[" in json) { "expected array wire form, got $json" } + } + + @Test + fun `SamplingMessage _meta round-trips`() { + val meta = buildJsonObject { put("k", JsonPrimitive("v")) } + val m = SamplingMessage(role = Role.User, content = listOf(TextContent("hi")), meta = meta) + val json = McpJson.encodeToString(SamplingMessage.serializer(), m) + val decoded = McpJson.decodeFromString(SamplingMessage.serializer(), json) + decoded.meta shouldBe meta + } + + // ============================================================================ + // CreateMessageRequestParams: tools / toolChoice + StopReason.ToolUse + // ============================================================================ + + @Test + fun `CreateMessageRequestParams tools and toolChoice default to null`() { + val params = CreateMessageRequestParams(maxTokens = 100, messages = emptyList()) + params.tools shouldBe null + params.toolChoice shouldBe null + } + + @Test + fun `CreateMessageRequestParams round-trips tools and toolChoice`() { + val original = CreateMessageRequestParams( + maxTokens = 100, + messages = emptyList(), + tools = listOf(dummyTool), + toolChoice = ToolChoice(mode = ToolChoice.Mode.Required), + ) + val encoded = McpJson.encodeToString(CreateMessageRequestParams.serializer(), original) + val decoded = McpJson.decodeFromString(CreateMessageRequestParams.serializer(), encoded) + decoded.tools?.single()?.name shouldBe "get_weather" + decoded.toolChoice shouldBe ToolChoice(mode = ToolChoice.Mode.Required) + } + + @Test + fun `StopReason ToolUse serialises as toolUse`() { + StopReason.ToolUse.value shouldBe "toolUse" + } + + // ============================================================================ + // CreateMessageResult shape (SEP-1577) + // ============================================================================ + + @Test + fun `CreateMessageResult single-block content serialises as single object`() { + val r = CreateMessageResult( + role = Role.Assistant, + content = TextContent("42"), + model = "test-model", + stopReason = StopReason.EndTurn, + ) + val json = McpJson.encodeToString(CreateMessageResult.serializer(), r) + check("\"content\":{" in json) { "expected single-object content wire form, got $json" } + check("\"content\":[" !in json) { "expected NOT to use array wire form for size-1, got $json" } + } + + @Test + fun `CreateMessageResult multi-block content with ToolUse stopReason round-trips`() { + val r = CreateMessageResult( + role = Role.Assistant, + content = listOf( + TextContent("Let me use a tool"), + ToolUseContent(id = "c1", name = "get_weather", input = JsonObject(emptyMap())), + ), + model = "test-model", + stopReason = StopReason.ToolUse, + ) + val json = McpJson.encodeToString(CreateMessageResult.serializer(), r) + val decoded = McpJson.decodeFromString(CreateMessageResult.serializer(), json) + decoded shouldBe r + decoded.stopReason shouldBe StopReason.ToolUse + decoded.content.size shouldBe 2 + } + + @Test + fun `CreateMessageResult pre-SEP single-object wire decodes correctly`() { + val json = """{"role":"assistant","content":{"type":"text","text":"hi"},"model":"m"}""" + val decoded = McpJson.decodeFromString(CreateMessageResult.serializer(), json) + decoded.content.size shouldBe 1 + (decoded.content[0] as TextContent).text shouldBe "hi" + } + + // ============================================================================ + // SamplingContentSerializer (single-or-array wire heuristic) + // ============================================================================ + + @Test + fun `SamplingContentSerializer decodes single object into list of one`() { + val json = """{"content":{"type":"text","text":"hi"}}""" + val h = McpJson.decodeFromString(Holder.serializer(), json) + h.content.size shouldBe 1 + h.content[0].shouldBeInstanceOf().text shouldBe "hi" + } + + @Test + fun `SamplingContentSerializer decodes array into list`() { + val json = """{"content":[{"type":"text","text":"a"},{"type":"text","text":"b"}]}""" + val h = McpJson.decodeFromString(Holder.serializer(), json) + h.content.size shouldBe 2 + } + + @Test + fun `SamplingContentSerializer encodes list of size one as a single object`() { + val h = Holder(content = listOf(TextContent("hi"))) + val json = McpJson.encodeToString(Holder.serializer(), h) + json shouldBe """{"content":{"text":"hi","type":"text"}}""" + } + + @Test + fun `SamplingContentSerializer encodes list of size two as an array`() { + val h = Holder(content = listOf(TextContent("a"), TextContent("b"))) + val json = McpJson.encodeToString(Holder.serializer(), h) + json shouldBe """{"content":[{"text":"a","type":"text"},{"text":"b","type":"text"}]}""" + } + + @Test + fun `SamplingContentSerializer encoding an empty list throws`() { + val h = Holder(content = emptyList()) + assertFailsWith { + McpJson.encodeToString(Holder.serializer(), h) + } + } + + @Test + fun `SamplingContentSerializer decoding an empty array throws`() { + val json = """{"content":[]}""" + assertFailsWith { + McpJson.decodeFromString(Holder.serializer(), json) + } + } + + // ============================================================================ + // ToolChoice + // ============================================================================ + + @Test + fun `ToolChoice round-trips auto mode`() { + val original = ToolChoice(mode = ToolChoice.Mode.Auto) + val json = McpJson.encodeToString(ToolChoice.serializer(), original) + json shouldBe """{"mode":"auto"}""" + McpJson.decodeFromString(ToolChoice.serializer(), json) shouldBe original + } + + @Test + fun `ToolChoice round-trips required mode`() { + val original = ToolChoice(mode = ToolChoice.Mode.Required) + McpJson.decodeFromString( + ToolChoice.serializer(), + McpJson.encodeToString(ToolChoice.serializer(), original), + ) shouldBe original + } + + @Test + fun `ToolChoice round-trips none mode`() { + val original = ToolChoice(mode = ToolChoice.Mode.None) + McpJson.decodeFromString( + ToolChoice.serializer(), + McpJson.encodeToString(ToolChoice.serializer(), original), + ) shouldBe original + } + + @Test + fun `ToolChoice absent mode serialises as empty object and deserialises to null mode`() { + val original = ToolChoice() + val json = McpJson.encodeToString(ToolChoice.serializer(), original) + json shouldBe """{}""" + McpJson.decodeFromString(ToolChoice.serializer(), json) shouldBe ToolChoice(mode = null) + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt index 53dd5c79f..ee2fe434c 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt @@ -4,10 +4,10 @@ import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.buildInitializeRequest import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.double import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlin.test.Test @@ -42,11 +42,7 @@ class CapabilitiesDslTest { val request = buildInitializeRequest { protocolVersion = "2024-11-05" capabilities { - sampling { - put("temperature", 0.7) - put("maxTokens", 1000) - put("topP", 0.95) - } + sampling(ClientCapabilities.Sampling(tools = EmptyJsonObject)) roots(listChanged = true) elicitation { put("mode", "interactive") @@ -72,9 +68,7 @@ class CapabilitiesDslTest { request.params.capabilities.shouldNotBeNull { sampling shouldNotBeNull { - get("temperature")?.jsonPrimitive?.double shouldBe 0.7 - get("maxTokens")?.jsonPrimitive?.content shouldBe "1000" - get("topP")?.jsonPrimitive?.double shouldBe 0.95 + tools shouldBe EmptyJsonObject } roots shouldNotBeNull { listChanged shouldBe true @@ -174,8 +168,8 @@ class CapabilitiesDslTest { val request = buildInitializeRequest { protocolVersion = "2024-11-05" capabilities { - sampling { put("temperature", 0.5) } - sampling { put("temperature", 0.9) } // Should overwrite + sampling(ClientCapabilities.Sampling()) + sampling(ClientCapabilities.Sampling(tools = EmptyJsonObject)) // Should overwrite experimental { put("feature", "v1") } experimental { put("feature", "v2") } // Should overwrite } @@ -184,7 +178,7 @@ class CapabilitiesDslTest { request.params.capabilities.shouldNotBeNull { sampling shouldNotBeNull { - get("temperature")?.jsonPrimitive?.double shouldBe 0.9 + tools shouldBe EmptyJsonObject } experimental shouldNotBeNull { get("feature")?.jsonPrimitive?.content shouldBe "v2" diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt index 180a30829..4801d02e1 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt @@ -42,7 +42,7 @@ class ContentDslTest { } } - (request.params.messages[0].content as TextContent).shouldNotBeNull { + (request.params.messages[0].content.single() as TextContent).shouldNotBeNull { text shouldBe "Hello, world!" annotations.shouldBeNull() meta.shouldBeNull() @@ -71,7 +71,7 @@ class ContentDslTest { } } - (request.params.messages[0].content as TextContent).shouldNotBeNull { + (request.params.messages[0].content.single() as TextContent).shouldNotBeNull { text shouldBe "Analyze the quarterly sales report for Q4 2025" annotations shouldNotBeNull { audience shouldBe listOf(Role.User, Role.Assistant) @@ -98,7 +98,7 @@ class ContentDslTest { } } - (request.params.messages[0].content as TextContent).text shouldBe "Hello 🌍! Ça va? Москва 北京 مرحبا" + (request.params.messages[0].content.single() as TextContent).text shouldBe "Hello 🌍! Ça va? Москва 北京 مرحبا" } @Test @@ -112,7 +112,7 @@ class ContentDslTest { } } - (request.params.messages[0].content as TextContent).text shouldBe "" + (request.params.messages[0].content.single() as TextContent).text shouldBe "" } @Test @@ -144,7 +144,7 @@ class ContentDslTest { } } - (request.params.messages[0].content as ImageContent).shouldNotBeNull { + (request.params.messages[0].content.single() as ImageContent).shouldNotBeNull { data shouldBe "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" mimeType shouldBe "image/png" @@ -177,7 +177,7 @@ class ContentDslTest { } } - (request.params.messages[0].content as ImageContent).shouldNotBeNull { + (request.params.messages[0].content.single() as ImageContent).shouldNotBeNull { mimeType shouldBe "image/jpeg" annotations shouldNotBeNull { audience shouldBe listOf(Role.Assistant) @@ -208,7 +208,7 @@ class ContentDslTest { } } } - (request.params.messages[0].content as ImageContent).mimeType shouldBe mime + (request.params.messages[0].content.single() as ImageContent).mimeType shouldBe mime } } @@ -256,7 +256,7 @@ class ContentDslTest { } } - (request.params.messages[0].content as AudioContent).shouldNotBeNull { + (request.params.messages[0].content.single() as AudioContent).shouldNotBeNull { data shouldBe "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA==" mimeType shouldBe "audio/wav" annotations.shouldBeNull() @@ -288,7 +288,7 @@ class ContentDslTest { } } - (request.params.messages[0].content as AudioContent).shouldNotBeNull { + (request.params.messages[0].content.single() as AudioContent).shouldNotBeNull { mimeType shouldBe "audio/mpeg" annotations shouldNotBeNull { audience shouldBe listOf(Role.User) @@ -349,7 +349,7 @@ class ContentDslTest { } } } - (requestMin.params.messages[0].content as TextContent).annotations?.priority shouldBe 0.0 + (requestMin.params.messages[0].content.single() as TextContent).annotations?.priority shouldBe 0.0 // Priority = 1.0 (maximum) val requestMax = buildCreateMessageRequest { @@ -361,7 +361,7 @@ class ContentDslTest { } } } - (requestMax.params.messages[0].content as TextContent).annotations?.priority shouldBe 1.0 + (requestMax.params.messages[0].content.single() as TextContent).annotations?.priority shouldBe 1.0 } @Test @@ -376,7 +376,7 @@ class ContentDslTest { } } - (request.params.messages[0].content as TextContent).annotations shouldNotBeNull { + (request.params.messages[0].content.single() as TextContent).annotations shouldNotBeNull { priority shouldBe 0.5 audience shouldBe listOf(Role.User) lastModified.shouldBeNull() diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt index aa53cce9d..17e5ae61d 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt @@ -9,7 +9,6 @@ import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.buildInitializeRequest import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlin.test.Test @@ -21,9 +20,7 @@ class InitializeDslTest { val request = buildInitializeRequest { protocolVersion = "2024-11-05" capabilities { - sampling { - put("maxTokens", 100) - } + sampling(ClientCapabilities.Sampling()) roots(listChanged = true) elicitation { put("mode", "interactive") @@ -47,7 +44,7 @@ class InitializeDslTest { request.params.protocolVersion shouldBe "2024-11-05" request.params.capabilities.shouldNotBeNull { - sampling?.get("maxTokens")?.jsonPrimitive?.int shouldBe 100 + sampling shouldNotBeNull { } roots?.listChanged shouldBe true elicitation?.get("mode")?.jsonPrimitive?.content shouldBe "interactive" experimental?.get("custom")?.jsonPrimitive?.content shouldBe "true" @@ -79,8 +76,8 @@ class InitializeDslTest { } @Test - fun `capabilities DSL should support direct JsonObject values`() { - val samplingObj = buildJsonObject { put("key", "value") } + fun `capabilities DSL should support direct typed values`() { + val samplingValue = ClientCapabilities.Sampling(tools = EmptyJsonObject) val elicitationObj = buildJsonObject { put("key", "value") } val experimentalObj = buildJsonObject { put("key", "value") } val extensionsMap = mapOf( @@ -90,7 +87,7 @@ class InitializeDslTest { val request = buildInitializeRequest { protocolVersion = "1.0" capabilities { - sampling(samplingObj) + sampling(samplingValue) elicitation(elicitationObj) experimental(experimentalObj) extensions(extensionsMap) @@ -99,7 +96,7 @@ class InitializeDslTest { } request.params.capabilities.shouldNotBeNull { - sampling shouldBe samplingObj + sampling shouldBe samplingValue elicitation shouldBe elicitationObj experimental shouldBe experimentalObj extensions shouldBe extensionsMap diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt index c53a4008c..10be8b094 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt @@ -71,7 +71,7 @@ class SamplingDslTest { request.params.stopSequences shouldBe listOf("STOP") request.params.messages shouldHaveSize 5 - (request.params.messages[2].content as TextContent).shouldNotBeNull { + (request.params.messages[2].content.single() as TextContent).shouldNotBeNull { text shouldBe "Text with annotations" annotations shouldNotBeNull { audience shouldBe listOf(Role.User) @@ -81,13 +81,13 @@ class SamplingDslTest { meta?.get("key")?.jsonPrimitive?.content shouldBe "value" } - (request.params.messages[3].content as ImageContent).shouldNotBeNull { + (request.params.messages[3].content.single() as ImageContent).shouldNotBeNull { data shouldBe "base64image" mimeType shouldBe "image/png" annotations?.priority shouldBe 0.5 } - (request.params.messages[4].content as AudioContent).shouldNotBeNull { + (request.params.messages[4].content.single() as AudioContent).shouldNotBeNull { data shouldBe "base64audio" mimeType shouldBe "audio/wav" } @@ -102,7 +102,7 @@ class SamplingDslTest { @Test fun `buildCreateMessageRequest should support direct assignments`() { - val messages = listOf(SamplingMessage(Role.User, TextContent("Hello"))) + val messages = listOf(SamplingMessage(Role.User, listOf(TextContent("Hello")))) val preferences = ModelPreferences(costPriority = 0.1) val request = buildCreateMessageRequest { maxTokens = 100 @@ -126,8 +126,8 @@ class SamplingDslTest { assistant(content) } } - request.params.messages[0].content shouldBe content - request.params.messages[1].content shouldBe content + request.params.messages[0].content.single() shouldBe content + request.params.messages[1].content.single() shouldBe content } @Test diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt index 23dbe89a6..e64b0da81 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt @@ -11,6 +11,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.IncludeContext import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel @@ -200,13 +201,34 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client } override suspend fun createMessage(request: CreateMessageRequest, options: RequestOptions?): CreateMessageResult { - with(request.params) { - logger.debug { - "Creating message with ${messages.size} messages, maxTokens=$maxTokens, " + - "temperature=$temperature, " + - "systemPrompt=${if (systemPrompt != null) "present" else "absent"}" + val caps = session.clientCapabilities + val params = request.params + + if (params.tools != null || params.toolChoice != null) { + requireNotNull(caps?.sampling?.tools) { + "Client did not advertise sampling.tools capability; cannot send " + + "tools/toolChoice in sampling/createMessage request." + } + } + + if (params.includeContext != null && params.includeContext != IncludeContext.None) { + if (caps?.sampling?.context == null) { + logger.warn { + "Client did not advertise sampling.context capability but server requested " + + "includeContext=${params.includeContext}. This is soft-deprecated and may be " + + "rejected by future spec versions." + } } } + + validateSamplingMessages(params.messages) + + logger.debug { + "Creating message with ${params.messages.size} messages, maxTokens=${params.maxTokens}, " + + "temperature=${params.temperature}, " + + "systemPrompt=${if (params.systemPrompt != null) "present" else "absent"}, " + + "tools=${params.tools?.size ?: 0}, toolChoice=${params.toolChoice?.mode}" + } logger.trace { "Full createMessage params: $request" } return request(request, options) } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt new file mode 100644 index 000000000..ef408080a --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt @@ -0,0 +1,51 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.ToolResultContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolUseContent + +/** + * Validates tool_use / tool_result rules on the last two messages of [messages]. + * + * Only the boundary between the previous message and the final message matters, because + * earlier turns were already validated when they were appended; the sole freshly-built + * portion of a sampling request is its tail. + * + * Rules enforced: + * + * 1. If the last message contains any `tool_result` block, it MUST contain only + * `tool_result` blocks (no mixing with text/image/audio/tool_use). + * 2. If the last message contains any `tool_result`, the previous message MUST contain + * matching `tool_use` blocks. + * 3. If the previous message contains `tool_use` blocks, the last message's + * `tool_result` ids MUST form exactly the same set. + * + * On the first violation throws [IllegalArgumentException]. No-op when there are fewer + * than two messages or no tool_use / tool_result blocks are involved. + */ +internal fun validateSamplingMessages(messages: List) { + if (messages.isEmpty()) return + + val last = messages.last().content + val hasToolResult = last.any { it is ToolResultContent } + + val previous = messages.getOrNull(messages.size - 2)?.content.orEmpty() + val hasPreviousToolUse = previous.any { it is ToolUseContent } + + if (hasToolResult) { + require(last.all { it is ToolResultContent }) { + "The last message must contain only tool_result content if any is present" + } + require(hasPreviousToolUse) { + "tool_result blocks are not matching any tool_use from the previous message" + } + } + + if (hasPreviousToolUse) { + val toolUseIds = previous.filterIsInstance().map { it.id }.toSet() + val toolResultIds = last.filterIsInstance().map { it.toolUseId }.toSet() + require(toolUseIds == toolResultIds) { + "ids of tool_result blocks and tool_use blocks from previous message do not match" + } + } +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt new file mode 100644 index 000000000..2b5446bfa --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt @@ -0,0 +1,86 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolResultContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolUseContent +import kotlinx.serialization.json.JsonObject +import org.junit.jupiter.api.assertDoesNotThrow +import kotlin.test.Test +import kotlin.test.assertFailsWith + +/** + * Unit tests for [validateSamplingMessages]: pure-function checks of the SEP-1577 + * last-two-messages tool_use / tool_result rules. + * + * End-to-end and capability-enforcement tests for `Server.createMessage` live in the + * `integration-test` module under the same package. + */ +class SamplingTest { + + private fun toolUse(id: String) = ToolUseContent(id = id, name = "t", input = JsonObject(emptyMap())) + private fun toolResult(id: String) = ToolResultContent(toolUseId = id, content = emptyList()) + + @Test + fun `validate empty message list is valid`() { + assertDoesNotThrow { validateSamplingMessages(emptyList()) } + } + + @Test + fun `validate text-only conversation is valid`() { + assertDoesNotThrow { + validateSamplingMessages( + listOf( + SamplingMessage(Role.User, TextContent("hi")), + SamplingMessage(Role.Assistant, TextContent("hello")), + ), + ) + } + } + + @Test + fun `validate matched tool_use and tool_result at boundary is valid`() { + assertDoesNotThrow { + validateSamplingMessages( + listOf( + SamplingMessage(Role.Assistant, listOf(TextContent("using tool"), toolUse("c1"))), + SamplingMessage(Role.User, toolResult("c1")), + ), + ) + } + } + + @Test + fun `validate orphan tool_result with no previous message fails`() { + assertFailsWith { + validateSamplingMessages( + listOf(SamplingMessage(Role.User, toolResult("missing"))), + ) + } + } + + @Test + fun `validate tool_result mixed with text in last message fails`() { + assertFailsWith { + validateSamplingMessages( + listOf( + SamplingMessage(Role.Assistant, toolUse("c1")), + SamplingMessage(Role.User, listOf(toolResult("c1"), TextContent("extra"))), + ), + ) + } + } + + @Test + fun `validate tool_result ids must match tool_use ids in previous message`() { + assertFailsWith { + validateSamplingMessages( + listOf( + SamplingMessage(Role.Assistant, toolUse("c1")), + SamplingMessage(Role.User, toolResult("wrong_id")), + ), + ) + } + } +} From 66d9efb59917d341d84ef71aedfb905ccc2c72c6 Mon Sep 17 00:00:00 2001 From: johny <69754601+jskjw157@users.noreply.github.com> Date: Wed, 29 Apr 2026 02:12:28 +0900 Subject: [PATCH 115/187] test(streamable-http): add missing integration tests for pagination, bad request, and logging (#613) This PR adds the remaining non-security integration tests for the Streamable HTTP transport as outlined in #183 and left over from #486. ### Changes included: - **Pagination**: Added cursor-based pagination tests for Prompts, Resources, and Tools to `Abstract*IntegrationTest`. - **Bad Request**: Added tests to verify that invalid cursor parameters correctly throw an `McpException` with `INTERNAL_ERROR`. - **Logging**: Added `LoggingIntegrationTestStreamableHttp` to specifically test `setLevel` and logging message notifications over Streamable HTTP. *Note: Security (Allow/Deny) tests are intentionally excluded as discussed in #486, pending further server auth model implementation.* Co-authored-by: Pavel Gorgulov --- .../kotlin/AbstractPromptIntegrationTest.kt | 73 +++++++++++++++++ .../kotlin/AbstractResourceIntegrationTest.kt | 79 ++++++++++++++++++ .../kotlin/AbstractToolIntegrationTest.kt | 80 +++++++++++++++++++ 3 files changed, 232 insertions(+) diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt index d14ae88e8..eaa51ada1 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt @@ -6,9 +6,15 @@ import io.kotest.matchers.string.shouldContain import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.Prompt import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.types.RPCError import io.modelcontextprotocol.kotlin.sdk.types.Role import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.TextContent @@ -18,7 +24,9 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import java.util.concurrent.CopyOnWriteArrayList import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -697,4 +705,69 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() { exception.message shouldBe expectedMessage } } + + @Test + fun testListPromptsPagination() = runBlocking(Dispatchers.IO) { + val receivedCursors = CopyOnWriteArrayList() + val page1 = listOf(Prompt(name = "p-1", description = "desc"), Prompt(name = "p-2", description = "desc")) + val page2 = listOf(Prompt(name = "p-3", description = "desc"), Prompt(name = "p-4", description = "desc")) + val page3 = listOf(Prompt(name = "p-5", description = "desc")) + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.PromptsList) { request, _ -> + receivedCursors += request.cursor + when (request.cursor) { + null -> ListPromptsResult(prompts = page1, nextCursor = "cursor-2") + "cursor-2" -> ListPromptsResult(prompts = page2, nextCursor = "cursor-3") + "cursor-3" -> ListPromptsResult(prompts = page3, nextCursor = null) + else -> error("Unexpected cursor: ${request.cursor}") + } + } + } + + val collected = mutableListOf() + var cursor: String? = null + do { + val request = if (cursor == null) { + ListPromptsRequest() + } else { + ListPromptsRequest(PaginatedRequestParams(cursor = cursor)) + } + val response = client.listPrompts(request) + collected += response.prompts + cursor = response.nextCursor + } while (cursor != null) + + assertEquals( + listOf(null, "cursor-2", "cursor-3"), + receivedCursors.toList(), + "Client must forward each nextCursor into the next request", + ) + assertEquals( + listOf("p-1", "p-2", "p-3", "p-4", "p-5"), + collected.map { it.name }, + "Client must accumulate pages in order without duplicates or reordering", + ) + } + + @Test + fun testListPromptsInvalidCursor() = runBlocking(Dispatchers.IO) { + val invalidCursor = "not-a-valid-cursor" + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.PromptsList) { _, _ -> + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Invalid cursor: $invalidCursor", + ) + } + } + + val exception = assertFailsWith { + client.listPrompts(ListPromptsRequest(PaginatedRequestParams(cursor = invalidCursor))) + } + + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + assertEquals("Invalid cursor: $invalidCursor", exception.message) + } } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt index 4fc53a52c..da7d85b35 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt @@ -1,11 +1,16 @@ package io.modelcontextprotocol.kotlin.sdk.integration.kotlin import io.modelcontextprotocol.kotlin.sdk.types.BlobResourceContents +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams import io.modelcontextprotocol.kotlin.sdk.types.RPCError import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.types.Resource import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequestParams @@ -18,8 +23,10 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -309,4 +316,76 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { assertTrue(result.contents.isNotEmpty(), "Result contents should not be empty") } } + + @Test + fun testListResourcesPagination() = runBlocking(Dispatchers.IO) { + val receivedCursors = CopyOnWriteArrayList() + val page1 = listOf( + Resource(uri = "test://r-1", name = "r-1"), + Resource(uri = "test://r-2", name = "r-2"), + Resource(uri = "test://r-3", name = "r-3"), + ) + val page2 = listOf( + Resource(uri = "test://r-4", name = "r-4"), + Resource(uri = "test://r-5", name = "r-5"), + ) + val page3 = listOf(Resource(uri = "test://r-6", name = "r-6")) + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.ResourcesList) { request, _ -> + receivedCursors += request.cursor + when (request.cursor) { + null -> ListResourcesResult(resources = page1, nextCursor = "cursor-2") + "cursor-2" -> ListResourcesResult(resources = page2, nextCursor = "cursor-3") + "cursor-3" -> ListResourcesResult(resources = page3, nextCursor = null) + else -> error("Unexpected cursor: ${request.cursor}") + } + } + } + + val collected = mutableListOf() + var cursor: String? = null + do { + val request = if (cursor == null) { + ListResourcesRequest() + } else { + ListResourcesRequest(PaginatedRequestParams(cursor = cursor)) + } + val response = client.listResources(request) + collected += response.resources + cursor = response.nextCursor + } while (cursor != null) + + assertEquals( + listOf(null, "cursor-2", "cursor-3"), + receivedCursors.toList(), + "Client must forward each nextCursor into the next request", + ) + assertEquals( + listOf("test://r-1", "test://r-2", "test://r-3", "test://r-4", "test://r-5", "test://r-6"), + collected.map { it.uri }, + "Client must accumulate pages in order without duplicates or reordering", + ) + } + + @Test + fun testListResourcesInvalidCursor() = runBlocking(Dispatchers.IO) { + val invalidCursor = "not-a-valid-cursor" + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.ResourcesList) { _, _ -> + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Invalid cursor: $invalidCursor", + ) + } + } + + val exception = assertFailsWith { + client.listResources(ListResourcesRequest(PaginatedRequestParams(cursor = invalidCursor))) + } + + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + assertEquals("Invalid cursor: $invalidCursor", exception.message) + } } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt index 7da82cc3a..b80212293 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt @@ -6,8 +6,15 @@ import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.ContentBlock import io.modelcontextprotocol.kotlin.sdk.types.ImageContent +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.RPCError import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.Tool import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -24,7 +31,9 @@ import org.junit.jupiter.api.Test import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.Locale +import java.util.concurrent.CopyOnWriteArrayList import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -791,4 +800,75 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() { "Error message should indicate the tool was not found", ) } + + @Test + fun testListToolsPagination() = runBlocking(Dispatchers.IO) { + val receivedCursors = CopyOnWriteArrayList() + val page1 = listOf( + Tool(name = "t-1", inputSchema = ToolSchema()), + Tool(name = "t-2", inputSchema = ToolSchema()), + ) + val page2 = listOf( + Tool(name = "t-3", inputSchema = ToolSchema()), + Tool(name = "t-4", inputSchema = ToolSchema()), + ) + val page3 = listOf(Tool(name = "t-5", inputSchema = ToolSchema())) + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.ToolsList) { request, _ -> + receivedCursors += request.cursor + when (request.cursor) { + null -> ListToolsResult(tools = page1, nextCursor = "cursor-2") + "cursor-2" -> ListToolsResult(tools = page2, nextCursor = "cursor-3") + "cursor-3" -> ListToolsResult(tools = page3, nextCursor = null) + else -> error("Unexpected cursor: ${request.cursor}") + } + } + } + + val collected = mutableListOf() + var cursor: String? = null + do { + val request = if (cursor == null) { + ListToolsRequest() + } else { + ListToolsRequest(PaginatedRequestParams(cursor = cursor)) + } + val response = client.listTools(request) + collected += response.tools + cursor = response.nextCursor + } while (cursor != null) + + assertEquals( + listOf(null, "cursor-2", "cursor-3"), + receivedCursors.toList(), + "Client must forward each nextCursor into the next request", + ) + assertEquals( + listOf("t-1", "t-2", "t-3", "t-4", "t-5"), + collected.map { it.name }, + "Client must accumulate pages in order without duplicates or reordering", + ) + } + + @Test + fun testListToolsInvalidCursor() = runBlocking(Dispatchers.IO) { + val invalidCursor = "not-a-valid-cursor" + + server.sessions.forEach { (_, session) -> + session.setRequestHandler(Method.Defined.ToolsList) { _, _ -> + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Invalid cursor: $invalidCursor", + ) + } + } + + val exception = assertFailsWith { + client.listTools(ListToolsRequest(PaginatedRequestParams(cursor = invalidCursor))) + } + + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + assertEquals("Invalid cursor: $invalidCursor", exception.message) + } } From a238fc0a2ee5af6fc0fd1a080fa964ca79f4e6ff Mon Sep 17 00:00:00 2001 From: Amr Youssef Date: Wed, 29 Apr 2026 03:09:05 -0700 Subject: [PATCH 116/187] fix(server): evict stale GET SSE stream mapping on reconnect (#716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Motivation and Context When a GET SSE stream disconnects and the client reconnects on the same session, the new GET is silently rejected because the previous stream's `STANDALONE_SSE_STREAM_ID` entry in `streamsMapping` hasn't been cleaned up yet. Cleanup via `invokeOnCompletion` is asynchronous — there is a race window where the stale entry persists and blocks reconnection. Additionally, Ktor's `sse {}` handler commits 200 OK headers before `handleGetRequest` runs, so the intended `call.reject(HttpStatusCode.Conflict, ...)` cannot change the already-committed HTTP status. On all tested engines (test host, CIO, Netty), clients receive a 200 OK with an immediately-closed empty SSE stream rather than a 409 Conflict. Server-side `CallLogging` reports 409 (from the response object, not the wire status), which is misleading. ### What this PR does 1. **Explicitly closes the old SSE session** before establishing a new stream. If the old session is already dead, `close()` is a no-op. This is the synchronous cleanup that the async `invokeOnCompletion` couldn't provide. 2. **Replaces `invokeOnCompletion` with `try/finally`** around `awaitCancellation()` — cleanup runs during cancellation propagation (earlier in the lifecycle) rather than after job completion. An identity check (`=== newContext`) ensures only this stream's entry is removed, not a replacement's. 3. **Adds identity-based removal in `emitOnStream`** — a failed `send()` on an old stream can no longer accidentally evict the new stream's mapping. 4. **Rethrows `CancellationException`** instead of swallowing it, preserving structured concurrency. The TypeScript reference implementation avoids this race via synchronous `ReadableStream.cancel()` cleanup. The Kotlin coroutine equivalent (`invokeOnCompletion`) is inherently async. The Go SDK uses `defer stream.release()` under a mutex for the same reason. The Java SDK uses unconditional `AtomicReference.set()` replacement with `compareAndExchange` on close. Fixes #715 ## How Has This Been Tested? All tests run against latest `main` (`bf8dc6d`). ktlint and detekt pass clean. - 2 new unit tests (Ktor test host): reconnect after disconnect, concurrent stream takeover - 2 new integration tests (real CIO embedded server): same scenarios over real HTTP - Manual verification against a live Ktor+Netty MCP server (curl): confirmed 200 OK on the wire with immediately-closed stream on reconnect (pre-fix), live stream on reconnect (post-fix) - All existing server unit tests pass (76 total) - All existing integration tests pass (306 total) ## Breaking Changes The server no longer returns 409 Conflict for a second GET SSE stream on the same session. Instead, the old stream is explicitly closed and the new stream takes over. The 409 was already unreachable as an HTTP status — Ktor's `sse {}` handler commits 200 OK before the transport handler runs on all tested engines (test host, CIO, Netty), so clients never received 409 on the wire. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context This bug likely became reachable after #681 (April 10) fixed the Netty `appendSseHeaders` crash that previously prevented GET SSE from working at all on Netty. Cross-SDK comparison of how each SDK handles a second GET SSE on the same session: | SDK | Behavior | Cleanup mechanism | Stale mapping bug? | |-----|----------|-------------------|-------------------| | TypeScript (reference) | 409 Conflict | Synchronous `ReadableStream.cancel()` | No | | Go | 409 Conflict | `defer stream.release()` under mutex | No | | Java | Replaces (no 409) | `AtomicReference.set()` + `compareAndExchange` on close | No | | C# | 400 Bad Request | Boolean flag, never reset (no reconnect support) | No | | Kotlin (before fix) | 409 (unreachable on wire) | Async `invokeOnCompletion` | **Yes** | | Kotlin (this PR) | Closes old + replaces | `try/finally` + identity-based removal | No | --------- Co-authored-by: devcrocod --- .../StreamableHttpSseReconnectTest.kt | 171 ++++++++++++++++++ .../server/StreamableHttpServerTransport.kt | 54 ++++-- .../StreamableHttpServerTransportTest.kt | 101 +++++++++++ 3 files changed, 309 insertions(+), 17 deletions(-) create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt new file mode 100644 index 000000000..751f8dd5d --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt @@ -0,0 +1,171 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp + +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.prepareGet +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.utils.io.readUTF8Line +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import io.modelcontextprotocol.kotlin.test.utils.actualPort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertNotNull +import io.ktor.client.engine.cio.CIO as ClientCIO + +private const val SESSION_ID_HEADER = "mcp-session-id" +private const val PROTOCOL_VERSION_HEADER = "mcp-protocol-version" + +/** + * Integration tests for GET SSE stream reconnection using a real embedded CIO server. + * + * Verifies that the transport correctly evicts stale STANDALONE_SSE_STREAM_ID + * entries when a client reconnects after a disconnect, rather than silently + * rejecting the new stream. + */ +class StreamableHttpSseReconnectTest : AbstractStreamableHttpIntegrationTest() { + + /** + * Verifies that after a GET SSE stream disconnects and the client + * immediately reconnects, the server evicts the stale stream mapping + * and allows the new stream to succeed. + */ + @Test + fun `GET SSE reconnect after disconnect should succeed`(): Unit = runBlocking(Dispatchers.IO) { + var server: StreamableHttpTestServer? = null + var httpClient: HttpClient? = null + + try { + server = initTestServer("reconnect-test") + val port = server.ktorServer.actualPort() + val mcpUrl = "http://$URL:$port/mcp" + + httpClient = HttpClient(ClientCIO) { install(SSE) } + + // Step 1: Initialize session via POST + val initResponse = httpClient.post(mcpUrl) { + contentType(ContentType.Application.Json) + header( + HttpHeaders.Accept, + "${ContentType.Application.Json}, ${ContentType.Text.EventStream}", + ) + setBody(Json.encodeToString(buildInitPayload())) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = assertNotNull(initResponse.headers[SESSION_ID_HEADER]) + + // Step 2: Open GET SSE stream, consume the flush event, then disconnect + httpClient.prepareGet(mcpUrl) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(SESSION_ID_HEADER, sessionId) + header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.bodyAsChannel().readUTF8Line() + } + + // Step 3: Immediately reconnect. The transport detects that the + // previous stream's coroutine is no longer active and evicts the + // stale mapping, allowing the new stream to succeed. + httpClient.prepareGet(mcpUrl) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(SESSION_ID_HEADER, sessionId) + header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.headers[SESSION_ID_HEADER] shouldBe sessionId + + val channel = response.bodyAsChannel() + val firstLine = channel.readUTF8Line() + firstLine.shouldNotBeNull() + channel.isClosedForRead shouldBe false + } + } finally { + httpClient?.close() + server?.ktorServer?.stopSuspend(1000, 2000) + } + } + + /** + * Verifies that a second concurrent GET SSE stream on the same session + * closes the old stream and takes over. The new stream should be live. + */ + @Test + fun `concurrent GET SSE stream closes old stream and takes over`(): Unit = runBlocking(Dispatchers.IO) { + var server: StreamableHttpTestServer? = null + var httpClient: HttpClient? = null + + try { + server = initTestServer("takeover-test") + val port = server.ktorServer.actualPort() + val mcpUrl = "http://$URL:$port/mcp" + + httpClient = HttpClient(ClientCIO) { install(SSE) } + + // Step 1: Initialize session via POST + val initResponse = httpClient.post(mcpUrl) { + contentType(ContentType.Application.Json) + header( + HttpHeaders.Accept, + "${ContentType.Application.Json}, ${ContentType.Text.EventStream}", + ) + setBody(Json.encodeToString(buildInitPayload())) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = assertNotNull(initResponse.headers[SESSION_ID_HEADER]) + + // Step 2: Open first GET SSE stream and keep it open + httpClient.prepareGet(mcpUrl) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(SESSION_ID_HEADER, sessionId) + header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION) + }.execute { firstResponse -> + firstResponse.status shouldBe HttpStatusCode.OK + firstResponse.bodyAsChannel().readUTF8Line() + + // Step 3: Open a second GET — closes old stream, new one takes over + httpClient.prepareGet(mcpUrl) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(SESSION_ID_HEADER, sessionId) + header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION) + }.execute { secondResponse -> + secondResponse.status shouldBe HttpStatusCode.OK + secondResponse.headers[SESSION_ID_HEADER] shouldBe sessionId + + // New stream is alive + val secondChannel = secondResponse.bodyAsChannel() + val firstLine = secondChannel.readUTF8Line() + firstLine.shouldNotBeNull() + secondChannel.isClosedForRead shouldBe false + } + } + } finally { + httpClient?.close() + server?.ktorServer?.stopSuspend(1000, 2000) + } + } + + private fun buildInitPayload(): JSONRPCRequest = InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = "reconnect-test-client", version = "1.0.0"), + ), + ).toJSON() +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 497005a48..f2320012d 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -464,27 +464,40 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - if (STANDALONE_SSE_STREAM_ID in streamsMapping) { - call.reject( - HttpStatusCode.Conflict, - RPCError.ErrorCode.CONNECTION_CLOSED, - "Conflict: Only one SSE stream is allowed per session", - ) - return - } - // SSE headers (Content-Type, Cache-Control, Connection) are already set by the framework's SSE handler flushSse(sseSession) - streamsMapping[STANDALONE_SSE_STREAM_ID] = SessionContext(sseSession, call) + val newContext = SessionContext(sseSession, call) + streamMutex.withLock { + streamsMapping[STANDALONE_SSE_STREAM_ID]?.let { existingContext -> + // Close the previous SSE session. If alive, this cancels the old + // coroutine (which will hit its identity-guarded finally — that finally + // won't double-remove, since we replace the mapping below). + try { + existingContext.session?.close() + } catch (e: CancellationException) { + throw e + } catch (_: Exception) { + // Ignore — the old stream may already be closed. + } + // Evict the stale mapping — the old session is closed either way. + streamsMapping.remove(STANDALONE_SSE_STREAM_ID) + } + streamsMapping[STANDALONE_SSE_STREAM_ID] = newContext + } val clientProtocolVersion = call.request.header(MCP_PROTOCOL_VERSION_HEADER) ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION maybeSendPrimingEvent(STANDALONE_SSE_STREAM_ID, sseSession, clientProtocolVersion) - sseSession.coroutineContext.job.invokeOnCompletion { - streamsMapping.remove(STANDALONE_SSE_STREAM_ID) - } // Keep the SSE connection open until the client disconnects or the transport is closed. - // Without this, the Ktor sse{} handler returns immediately, closing the stream. - awaitCancellation() + // Cleanup uses try/finally (runs during cancellation propagation) instead of + // invokeOnCompletion (runs after job completion) to minimize the window between + // disconnect and mapping removal. Identity check ensures only this stream's entry + // is removed — not a replacement that arrived in the meantime. + try { + awaitCancellation() + } finally { + // Atomic compare-and-remove — only evict our own entry, not a replacement's. + streamsMapping.remove(STANDALONE_SSE_STREAM_ID, newContext) + } } /** Handles an HTTP DELETE request by closing the session and the transport. */ @@ -725,10 +738,17 @@ public class StreamableHttpServerTransport(private val configuration: Configurat try { session?.send(event = "message", id = eventId, data = McpJson.encodeToString(message)) } catch (e: CancellationException) { - streamsMapping.remove(streamId) + // Atomic compare-and-remove — only evict this stream's entry, not a replacement's. + val current = streamsMapping[streamId] + if (current != null && current.session === session) { + streamsMapping.remove(streamId, current) + } throw e } catch (_: Exception) { - streamsMapping.remove(streamId) + val current = streamsMapping[streamId] + if (current != null && current.session === session) { + streamsMapping.remove(streamId, current) + } } } diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt index d00e10ec5..8b413d8a5 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt @@ -460,6 +460,107 @@ class StreamableHttpServerTransportTest { } } + @Test + fun `second concurrent GET SSE closes old stream and takes over`() = testApplication { + val mcpPath = "/mcp" + + application { + mcpStreamableHttp(mcpPath) { + Server( + Implementation("test-server", "1.0.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + } + } + + val client = createTestClient() + + // Step 1: Initialize session via POST + val initResponse = client.post(mcpPath) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = assertNotNull(initResponse.headers[MCP_SESSION_ID_HEADER]) + + // Step 2: Open first GET SSE stream + client.prepareGet(mcpPath) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(MCP_SESSION_ID_HEADER, sessionId) + header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) + }.execute { firstResponse -> + firstResponse.status shouldBe HttpStatusCode.OK + firstResponse.bodyAsChannel().readUTF8Line() + + // Step 3: Open a second GET — the transport closes the old session + // and the new stream takes over. + client.prepareGet(mcpPath) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(MCP_SESSION_ID_HEADER, sessionId) + header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) + }.execute { secondResponse -> + secondResponse.status shouldBe HttpStatusCode.OK + secondResponse.headers[MCP_SESSION_ID_HEADER] shouldBe sessionId + + // New stream is alive + val secondChannel = secondResponse.bodyAsChannel() + val firstLine = secondChannel.readUTF8Line() + firstLine.shouldNotBeNull() + secondChannel.isClosedForRead shouldBe false + } + } + } + + @Test + fun `GET SSE reconnect after previous stream disconnects should succeed`() = testApplication { + val mcpPath = "/mcp" + + application { + mcpStreamableHttp(mcpPath) { + Server( + Implementation("test-server", "1.0.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + } + } + + val client = createTestClient() + + // Step 1: Initialize session via POST + val initResponse = client.post(mcpPath) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = assertNotNull(initResponse.headers[MCP_SESSION_ID_HEADER]) + + // Step 2: Open and then close a GET SSE stream + client.prepareGet(mcpPath) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(MCP_SESSION_ID_HEADER, sessionId) + header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.bodyAsChannel().readUTF8Line() + } + + // Step 3: Immediately reconnect — the transport should close the stale + // stream and allow the new one. + client.prepareGet(mcpPath) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(MCP_SESSION_ID_HEADER, sessionId) + header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.headers[MCP_SESSION_ID_HEADER] shouldBe sessionId + + val channel = response.bodyAsChannel() + val firstLine = channel.readUTF8Line() + firstLine.shouldNotBeNull() + channel.isClosedForRead shouldBe false + } + } + @Test fun `GET SSE stream includes Mcp-Session-Id header and stays open`() = testApplication { val mcpPath = "/mcp" From 64804a05c227389440f5a4c6cf2712f527b54b77 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 29 Apr 2026 12:31:49 +0200 Subject: [PATCH 117/187] refactor: explicit locked detekt config (#729) Migrate detekt to a convention plugin in `buildSrc`, switch the project to a fully explicit YAML config (`buildUponDefaultConfig = false`), fix the real findings that this stricter setup revealed, and remove `@Suppress` annotations that became unnecessary after the rule changes. ## Motivation and Context - Explicit visibility into the rule set - Lock against detekt version upgrades ## Breaking Changes None ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .claude/skills/kdoc/SKILL.md | 77 +- .github/workflows/build.yml | 2 +- build.gradle.kts | 13 - buildSrc/build.gradle.kts | 1 + .../src/main/kotlin/mcp.detekt.gradle.kts | 23 + .../main/kotlin/mcp.multiplatform.gradle.kts | 9 - config/detekt/detekt.yml | 855 +++++++++++++++++- .../sdk/conformance/ConformanceTools.kt | 1 - gradle/libs.versions.toml | 4 +- integration-test/build.gradle.kts | 1 + integration-test/detekt-baseline-test.xml | 17 - kotlin-sdk-client/build.gradle.kts | 1 + .../detekt-baseline-commonMainSourceSet.xml | 14 - .../detekt-baseline-commonTestSourceSet.xml | 8 - kotlin-sdk-client/detekt-baseline-main.xml | 14 +- kotlin-sdk-client/detekt-baseline-test.xml | 14 - .../kotlin/sdk/client/Client.kt | 19 +- .../kotlin/sdk/client/StdioClientTransport.kt | 3 +- .../client/StreamableHttpClientTransport.kt | 25 +- .../http/StreamableHttpClientTransportTest.kt | 1 - .../kotlin/sdk/client/MockMcp.kt | 4 - .../sdk/client/StreamableHttpClientTest.kt | 4 +- kotlin-sdk-core/build.gradle.kts | 1 + .../detekt-baseline-commonMainSourceSet.xml | 27 - kotlin-sdk-core/detekt-baseline-main.xml | 25 - .../sdk/shared/AbstractClientTransport.kt | 6 +- .../kotlin/sdk/shared/AbstractTransport.kt | 8 +- .../kotlin/sdk/shared/Protocol.kt | 61 +- .../kotlin/sdk/shared/ReadBuffer.kt | 3 +- .../kotlin/sdk/shared/Transport.kt | 6 +- .../sdk/shared/WebSocketMcpTransport.kt | 1 - .../kotlin/sdk/types/McpException.kt | 10 +- .../kotlin/sdk/types/PingRequest.kt | 3 +- .../kotlin/sdk/types/common.kt | 9 +- .../kotlin/sdk/types/completion.kt | 4 +- .../kotlin/sdk/types/content.kt | 6 +- .../kotlin/sdk/types/elicitation.kt | 15 +- .../kotlin/sdk/types/jsonRpc.kt | 11 +- .../kotlin/sdk/types/methods.kt | 3 +- .../kotlin/sdk/types/notification.kt | 12 +- .../kotlin/sdk/types/request.kt | 13 +- .../kotlin/sdk/types/resources.kt | 12 +- .../kotlin/sdk/types/roots.kt | 2 +- .../kotlin/sdk/types/schema.kt | 20 +- .../kotlin/sdk/types/serializers.kt | 4 - .../kotlin/sdk/types/tasks.kt | 23 +- .../kotlin/sdk/types/tools.kt | 2 +- .../sdk/utils/PathSegmentTemplateMatcher.kt | 9 +- .../sdk/utils/ResourceTemplateMatcher.kt | 3 +- .../kotlin/sdk/types/ElicitationTest.kt | 3 - .../kotlin/sdk/types/InitializeTest.kt | 1 - .../kotlin/sdk/types/PrimitiveSchemaTest.kt | 2 - .../kotlin/sdk/types/SamplingTest.kt | 1 - .../kotlin/sdk/types/ToolsTest.kt | 1 - .../kotlin/sdk/types/dsl/SamplingDslTest.kt | 1 - kotlin-sdk-server/build.gradle.kts | 1 + .../detekt-baseline-commonMainSourceSet.xml | 13 - kotlin-sdk-server/detekt-baseline-main.xml | 23 - .../kotlin/sdk/server/ClientConnection.kt | 2 - .../kotlin/sdk/server/Feature.kt | 1 + .../sdk/server/FeatureNotificationService.kt | 3 +- .../kotlin/sdk/server/KtorServer.kt | 4 - .../kotlin/sdk/server/SSEServerTransport.kt | 4 +- .../kotlin/sdk/server/Server.kt | 14 +- .../kotlin/sdk/server/ServerSession.kt | 25 +- .../kotlin/sdk/server/StdioServerTransport.kt | 4 - .../server/StreamableHttpServerTransport.kt | 14 +- .../WebSocketMcpKtorServerExtensions.kt | 2 - .../sdk/server/AbstractKtorExtensionsTest.kt | 1 - .../sdk/server/StdioServerTransportTest.kt | 3 - kotlin-sdk-testing/build.gradle.kts | 1 + .../kotlin/sdk/testing/ChannelTransport.kt | 20 +- .../sdk/testing/ChannelTransportTest.kt | 1 - .../kotlin/sdk/package.kt | 26 +- test-utils/build.gradle.kts | 1 + test-utils/detekt-baseline-main.xml | 20 + test-utils/detekt-baseline.xml | 8 - .../kotlin/test/utils/Retry.kt | 1 - .../kotlin/test/utils/TypeScriptRunner.kt | 2 - .../kotlin/test/utils/processUtils.kt | 1 - 80 files changed, 1106 insertions(+), 507 deletions(-) create mode 100644 buildSrc/src/main/kotlin/mcp.detekt.gradle.kts delete mode 100644 integration-test/detekt-baseline-test.xml delete mode 100644 kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml delete mode 100644 kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml delete mode 100644 kotlin-sdk-client/detekt-baseline-test.xml delete mode 100644 kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml delete mode 100644 kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml create mode 100644 test-utils/detekt-baseline-main.xml delete mode 100644 test-utils/detekt-baseline.xml diff --git a/.claude/skills/kdoc/SKILL.md b/.claude/skills/kdoc/SKILL.md index 476029367..cfacfc3f4 100644 --- a/.claude/skills/kdoc/SKILL.md +++ b/.claude/skills/kdoc/SKILL.md @@ -5,40 +5,62 @@ description: "Add KDoc documentation to Kotlin public API. Use whenever the user # KDoc Generator -Add KDoc comments to public Kotlin API declarations. +Add KDoc to all public Kotlin declarations (classes, interfaces, objects, functions, properties, enums, type aliases, annotations — including `@Deprecated`). Read the implementation, not just the signature, to write accurate descriptions. -## What to document +## Tag placement -All public declarations (no explicit `private`/`internal`/`protected`): -- Classes, interfaces, objects, sealed classes/interfaces -- Functions, extension functions -- Properties -- Enum classes and entries -- Type aliases, annotation classes +| Where the declaration lives | Tag | +|---|---| +| Primary-ctor `val` / `var` (public, internal, or protected) | `@property` in class KDoc | +| Primary-ctor `name: Type` (no `val`/`var`) | `@param` in class KDoc | +| Primary-ctor `private val` / `private var` | `@param` in class KDoc | +| Body of class/object/interface/sealed interface | inline `/** */` above the declaration | +| Function parameter | `@param` | +| Non-Unit return type | `@return` | -Include `@Deprecated` elements. +## Style -## Context +- Summary: one concise sentence, third-person verb ("Creates", "Returns", "Represents"). Expand to 2-3 sentences only when genuinely complex. +- **Example block**: mandatory for DSL builders (show full receiver-scope usage); add for complex functions or non-obvious extension functions; skip for one-liners and simple getters. +- KDoc links (`[ClassName]`, `[methodName]`) only where they add navigational value. +- Don't write `@throws` or "suspend" notes. +- Rewrite existing KDoc if incomplete or low quality. -Read the implementation, not just the signature, to write accurate descriptions. Understanding what the code actually does prevents superficial or misleading documentation. +## Examples -## KDoc format - -**Class/interface example:** +**Class with mixed primary-constructor parameters:** ````kotlin /** * Manages active client sessions and their lifecycle. * - * Sessions are created on first connection and cleaned up - * when the transport closes. - * * @property maxSessions upper limit on concurrent sessions - * @property timeout idle timeout before a session is evicted + * @param scope coroutine scope for session lifecycle tasks + * @param secret used internally for session token signing */ +public class SessionManager( + public val maxSessions: Int, // public val → @property + scope: CoroutineScope, // no val/var → @param + private val secret: ByteArray, // private val → @param +) ```` -**Function example:** +**Body and interface properties (inline `/** */`):** + +````kotlin +/** Represents an entity that includes additional metadata. */ +public interface WithMeta { + /** Optional metadata attached to this entity. */ + public val meta: JsonObject? +} + +public class Connection(public val id: String) { + /** Capabilities negotiated during the handshake, or `null` before initialize completes. */ + public val capabilities: ServerCapabilities? get() = ... +} +```` + +**Function with Example block:** ````kotlin /** @@ -46,10 +68,7 @@ Read the implementation, not just the signature, to write accurate descriptions. * * Example: * ```kotlin - * server.addTool( - * name = "echo", - * description = "Echoes input back", - * ) { request -> + * server.addTool(name = "echo", description = "Echoes input back") { request -> * CallToolResult(content = listOf(TextContent("Echo: ${request.arguments}"))) * } * ``` @@ -60,15 +79,3 @@ Read the implementation, not just the signature, to write accurate descriptions. * @return the registered tool definition */ ```` - -## Rules - -- Summary: concise first sentence starting with a third-person verb ("Creates", "Returns", "Represents"). Expand to 2-3 sentences only when genuinely complex -- **@property** in class-level KDoc for all public properties (never as individual KDoc on the property); **@param** for function parameters -- **@return** for non-Unit return types -- **Example block**: add for DSL builders, complex functions, extension functions with non-obvious usage. Skip for trivial one-liners and simple getters -- **DSL builders**: always include an Example showing full usage with the receiver scope — this is critical for discoverability -- KDoc links (`[ClassName]`, `[methodName]`): only where it adds clear navigational value -- No **@throws** — don't document exceptions -- No **suspend** notes — coroutine nature is visible from the signature -- **Existing KDoc**: rewrite if incomplete (missing @param/@return/@property) or low quality \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index caaa3f0d8..ad4cc7ae0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: include: - os: ubuntu-latest job-name: "Linux Tests" - gradle-tasks: "ktlintCheck build" + gradle-tasks: "build" max-workers: 3 - os: macos-latest diff --git a/build.gradle.kts b/build.gradle.kts index 72c55539b..2c2dbe3dd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,5 @@ -import dev.detekt.gradle.extensions.FailOnSeverity - plugins { id("mcp.dokka") - alias(libs.plugins.detekt) alias(libs.plugins.ktlint) alias(libs.plugins.kover) } @@ -23,16 +20,6 @@ dependencies { subprojects { apply(plugin = "org.jlleitschuh.gradle.ktlint") apply(plugin = "org.jetbrains.kotlinx.kover") - - if (name != "conformance-test" && name != "docs") { - apply(plugin = "dev.detekt") - - detekt { - config = files("$rootDir/config/detekt/detekt.yml") - buildUponDefaultConfig = true - failOnSeverity.set(FailOnSeverity.Error) - } - } } dokka { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 84676a4f5..4a057cefa 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,6 +8,7 @@ repositories { } dependencies { + implementation(libs.detekt.gradle) implementation(libs.kotlin.gradle) implementation(libs.kotlin.serialization) implementation(libs.kotlinx.atomicfu.gradle) diff --git a/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts b/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts new file mode 100644 index 000000000..3c1419e84 --- /dev/null +++ b/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts @@ -0,0 +1,23 @@ +import dev.detekt.gradle.Detekt +import dev.detekt.gradle.extensions.FailOnSeverity + +plugins { + id("dev.detekt") +} + +detekt { + config = files("$rootDir/config/detekt/detekt.yml") + buildUponDefaultConfig = false + parallel = true + failOnSeverity = FailOnSeverity.Error +} + +tasks.withType().configureEach { + exclude("**/generated/**", "**/generated-sources/**", "**/build/**") +} + +pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") { + tasks.named("detekt").configure { + dependsOn("detektMainJvm") + } +} diff --git a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts index 2903cff6a..98a0a5e1f 100644 --- a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts +++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts @@ -57,12 +57,3 @@ kotlin { explicitApi = ExplicitApiMode.Strict jvmToolchain(21) } - -tasks.named("detekt").configure { - dependsOn( - "detektMainJvm", - "detektCommonMainSourceSet", - "detektTestJvm", - "detektCommonTestSourceSet", - ) -} diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 4d4473430..98fce0ee8 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -3,41 +3,840 @@ config: warningsAsErrors: false checkExhaustiveness: false +processors: + active: false + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'IssuesReport' + - 'FileBasedIssuesReport' + # - 'LiteIssuesReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateIsRegex: false + licenseTemplate: '' + DeprecatedBlockTag: + active: true + DocumentationOverPrivateFunction: + active: false + DocumentationOverPrivateProperty: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + ignoreDefaultCompanionObject: true + UndocumentedPublicFunction: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false + ignoreEnumEntries: false + complexity: + active: true + CognitiveComplexMethod: + active: false + allowedComplexity: 20 + ComplexCondition: + active: true + allowedConditions: 4 + ComplexInterface: + active: false + allowedDefinitions: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: false + allowedComplexity: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + ignoreLocalFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [] LargeClass: - excludes: &testFolders - - '**/test/**' - - '**/commonTest/**' - - '**/jvmTest/**' - - '**/jsTest/**' - - '**/iosTest/**' + active: false + allowedLines: 700 LongMethod: - excludes: *testFolders + active: false + allowedLines: 110 + LongParameterList: + active: false + allowedFunctionParameters: 8 + allowedConstructorParameters: 8 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + allowedOverloads: 6 + NamedArguments: + active: false + allowedArguments: 3 + ignoreMethods: [] + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + allowedDepth: 4 + NestedScopeFunctions: + active: false + allowedDepth: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + allowedDuplications: 3 + ignoreAnnotation: true + allowedWithLengthLessThan: 5 + ignoreStringsRegex: '$^' + TooManyFunctions: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + allowedFunctionsPerFile: 20 + allowedFunctionsPerClass: 20 + allowedFunctionsPerInterface: 20 + allowedFunctionsPerObject: 20 + allowedFunctionsPerEnum: 11 + ignoreDeprecated: true + ignorePrivate: true + ignoreInternal: true + ignoreOverridden: false + ignoreAnnotatedFunctions: [] coroutines: + active: true + CoroutineLaunchedInTestWithoutRunTest: + active: false + GlobalCoroutineUsage: + active: true InjectDispatcher: - excludes: *testFolders + active: false + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunInFinallySection: + active: false + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + aliases: ['SuspendFunctionOnCoroutineScope'] + SuspendFunWithFlowReturnType: + active: true empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true EmptyFunctionBlock: - excludes: *testFolders + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKotlinFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true -comments: - UndocumentedPublicClass: - active: true - excludes: &testAndGeneratedFolders - - '**/test/**' - - '**/commonTest/**' - - '**/jvmTest/**' - - '**/jsTest/**' - - '**/iosTest/**' - - '**/generated-sources/**' - ignoreDefaultCompanionObject: true - UndocumentedPublicFunction: - active: true - excludes: *testAndGeneratedFolders - UndocumentedPublicProperty: - active: true - excludes: *testAndGeneratedFolders - searchProtectedProperty: false - ignoreEnumEntries: true +exceptions: + active: true + ErrorUsageWithThrowable: + active: false + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: true + ObjectExtendsThrowable: + active: true + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: false + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + aliases: ['ClassName'] + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + aliases: ['EnumEntryName'] + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionNameMaxLength: + active: false + aliases: ['FunctionMaxNameLength'] + maximumFunctionNameLength: 30 + FunctionNameMinLength: + active: false + aliases: ['FunctionMinNameLength'] + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + aliases: ['FunctionName'] + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + aliases: ['PackageDirectoryMismatch'] + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: false + mustBeFirst: true + multiplatformTargets: + - 'ios' + - 'android' + - 'js' + - 'jvm' + - 'native' + - 'iosArm64' + - 'iosX64' + - 'macosX64' + - 'mingwX64' + - 'linuxX64' + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: true + ObjectPropertyNaming: + active: true + aliases: ['ObjectPropertyName'] + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + aliases: ['PackageName'] + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: false + aliases: ['PropertyName'] + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + allowedOperations: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryInitOnArray: + active: false + UnnecessaryPartOfBinaryExpression: + active: true + UnnecessaryTemporaryInstantiation: + active: true + UnnecessaryTypeCasting: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: true + ignorePlatformTypes: true + CastToNullableType: + active: false + CharArrayToStringCall: + active: false + Deprecation: + active: false + aliases: ['DEPRECATION'] + excludeImportStatements: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + aliases: ['DoubleMutability'] + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.Function*' + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + ignoreAnnotated: ['Test'] + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: true + excludes: ['**/*.kts'] + MissingSuperCall: + active: false + mustInvokeSuperAnnotations: + - 'androidx.annotation.CallSuper' + - 'javax.annotation.OverridingMethodsMustInvokeSuper' + MissingUseCall: + active: false + ignoreClass: + - 'java.io.ByteArrayInputStream' + - 'java.io.ByteArrayOutputStream' + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnamedParameterUse: + active: false + allowAdjacentDifferentTypeParams: true + allowSingleParamUse: true + ignoreArgumentsMatchingNames: true + ignoreFunctionCall: [] + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + aliases: ['UNCHECKED_CAST'] + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AbstractClassCanBeConcreteClass: + active: true + AbstractClassCanBeInterface: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeExpression: + active: false + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaMultipleParameters: + active: true + ExplicitItLambdaParameter: + active: false + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: true + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + forbiddenImports: [] + allowedImports: [] + ForbiddenMethodCall: + active: true + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + - reason: 'using `BigDecimal(Double)` can result in unexpected floating point precision behavior. Use `BigDecimal.valueOf(Double)` or `String.toBigDecimalOrNull()` instead.' + value: 'java.math.BigDecimal.(kotlin.Double)' + - reason: 'using `BigDecimal(String)` can result in a `NumberFormatException`. Use `String.toBigDecimalOrNull()`' + value: 'java.math.BigDecimal.(kotlin.String)' + - reason: 'It is marked as obsolete. Use `kotlin.time.measureTime` instead.' + value: 'kotlin.system.measureTimeMillis' + ForbiddenNamedParam: + active: false + methods: [] + ForbiddenOptIn: + active: false + markerClasses: [] + ForbiddenSuppress: + active: false + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: true + ignoreLocalVariableDeclaration: true + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: false + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConstant: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + ProtectedMemberInFinalClass: + active: true + RangeUntilInsteadOfRangeTo: + active: true + RedundantConstructorKeyword: + active: false + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifier: + active: false + ReturnCount: + active: false + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingAfterPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: false + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAny: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryFullyQualifiedName: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UnnecessaryReversed: + active: false + UnusedImport: + active: false + additionalOperatorSet: [] + UnusedParameter: + active: true + aliases: ['UNUSED_PARAMETER', 'unused'] + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + aliases: ['unused'] + UnusedPrivateFunction: + active: true + aliases: ['unused'] + allowedNames: '' + UnusedPrivateProperty: + active: true + aliases: ['unused'] + allowedNames: 'ignored|expected|serialVersionUID' + UnusedVariable: + active: true + aliases: ['UNUSED_VARIABLE', 'unused'] + allowedNames: 'ignored|_' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + aliases: ['CanBeVal'] + ignoreLateinitVar: false + WildcardImport: + active: false + excludeImports: + - 'java.util.*' diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt index dc126f681..e288272ef 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt @@ -45,7 +45,6 @@ internal const val WAV_BASE64 = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABA private val logger = KotlinLogging.logger {} -@Suppress("LongMethod") fun Server.registerConformanceTools() { // 1. Simple text addTool( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca67d7e56..360a468bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # plugins version atomicfu = "0.32.1" binaryCompatibilityValidatorPlugin = "0.18.1" -detekt = "2.0.0-alpha.2" +detekt = "2.0.0-alpha.3" dokka = "2.2.0" kotlin = "2.3.21" kover = "0.9.8" @@ -28,6 +28,7 @@ junit = "5.14.3" [libraries] # Plugins +detekt-gradle = { module = "dev.detekt:detekt-gradle-plugin", version.ref = "detekt" } dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } @@ -75,7 +76,6 @@ ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = " ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" } [plugins] -detekt = { id = "dev.detekt", version.ref = "detekt" } kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidatorPlugin" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } diff --git a/integration-test/build.gradle.kts b/integration-test/build.gradle.kts index 30afe9e8a..7a3a4a3d2 100644 --- a/integration-test/build.gradle.kts +++ b/integration-test/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("mcp.multiplatform") + id("mcp.detekt") } kotlin { diff --git a/integration-test/detekt-baseline-test.xml b/integration-test/detekt-baseline-test.xml deleted file mode 100644 index 2df611a62..000000000 --- a/integration-test/detekt-baseline-test.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - AbstractClassCanBeConcreteClass:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$AbstractPromptIntegrationTest - AbstractClassCanBeConcreteClass:AbstractResourceIntegrationTest.kt:AbstractResourceIntegrationTest$AbstractResourceIntegrationTest - AbstractClassCanBeConcreteClass:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$AbstractToolIntegrationTest - AbstractClassCanBeConcreteClass:BaseTransportTest.kt:BaseTransportTest$BaseTransportTest - CyclomaticComplexMethod:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$private fun setupCalculatorTool - ForbiddenComment:StdioClientTransportTest.kt:StdioClientTransportTest$// TODO: fix running on windows - MatchingDeclarationName:PromptIntegrationTestSse.kt:SchemaPromptIntegrationTestSse : AbstractPromptIntegrationTest - ThrowsCount:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$override fun configureServer - UseCheckOrError:ClientTest.kt:ClientTest.<no name provided>$throw IllegalStateException("Test error") - UseOrEmpty:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$(request.params.arguments?.get("tags") as? JsonArray)?.mapNotNull { (it as? JsonPrimitive)?.content } ?: emptyList() - UseRequire:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$throw IllegalArgumentException("Missing required argument: $argName") - - diff --git a/kotlin-sdk-client/build.gradle.kts b/kotlin-sdk-client/build.gradle.kts index a3645b768..ae7a3149a 100644 --- a/kotlin-sdk-client/build.gradle.kts +++ b/kotlin-sdk-client/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("mcp.multiplatform") + id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) diff --git a/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml deleted file mode 100644 index 5cddbe13e..000000000 --- a/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - CyclomaticComplexMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize - LongMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize - MaxLineLength:StdioClientTransport.kt:StdioClientTransport$* - ThrowsCount:SseClientTransport.kt:SseClientTransport$private suspend fun CoroutineScope.collectMessages - ThrowsCount:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$@Suppress("ReturnCount", "CyclomaticComplexMethod") override suspend fun performSend - TooGenericExceptionCaught:Client.kt:Client$error: Throwable - TooGenericExceptionCaught:SseClientTransport.kt:SseClientTransport$e: Throwable - TooGenericExceptionCaught:StdioClientTransport.kt:StdioClientTransport$e: Throwable - - diff --git a/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml b/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml deleted file mode 100644 index cab432296..000000000 --- a/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""" - MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}""" - - diff --git a/kotlin-sdk-client/detekt-baseline-main.xml b/kotlin-sdk-client/detekt-baseline-main.xml index 8411ab0f1..c34e75ce9 100644 --- a/kotlin-sdk-client/detekt-baseline-main.xml +++ b/kotlin-sdk-client/detekt-baseline-main.xml @@ -2,18 +2,6 @@ - CyclomaticComplexMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize - InjectDispatcher:StdioClientTransport.kt:StdioClientTransport$Default - InjectDispatcher:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$Default - LongMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize - MaxLineLength:StdioClientTransport.kt:StdioClientTransport$* - NoNameShadowing:StdioClientTransport.kt:StdioClientTransport$source - ThrowsCount:SseClientTransport.kt:SseClientTransport$private suspend fun CoroutineScope.collectMessages - ThrowsCount:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$@Suppress("ReturnCount", "CyclomaticComplexMethod") override suspend fun performSend - TooGenericExceptionCaught:Client.kt:Client$error: Throwable - TooGenericExceptionCaught:SseClientTransport.kt:SseClientTransport$e: Throwable - TooGenericExceptionCaught:StdioClientTransport.kt:StdioClientTransport$e: Throwable - UseCheckOrError:Client.kt:Client$throw IllegalStateException( "Server does not support resource subscriptions (required for $method)", ) - UseCheckOrError:Client.kt:Client$throw IllegalStateException("Client does not support roots capability.") + LoopWithTooManyJumpStatements:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$while diff --git a/kotlin-sdk-client/detekt-baseline-test.xml b/kotlin-sdk-client/detekt-baseline-test.xml deleted file mode 100644 index dd6adfab3..000000000 --- a/kotlin-sdk-client/detekt-baseline-test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - AbstractClassCanBeConcreteClass:AbstractStreamableHttpClientTest.kt:AbstractStreamableHttpClientTest$AbstractStreamableHttpClientTest - ForbiddenComment:StreamableHttpClientTest.kt:StreamableHttpClientTest$// TODO: how to get notifications via Client API? - LongParameterList:MockMcp.kt:MockMcp$fun handleJSONRPCRequest - LongParameterList:MockMcp.kt:MockMcp$fun handleWithResult - LongParameterList:MockMcp.kt:MockMcp$fun onInitialize - MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""" - MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}""" - UseOrEmpty:ClientMetaParameterTest.kt:ClientMetaParameterTest$exception.message ?: "" - - diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index 49b20d7cb..d71c09182 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -83,7 +83,7 @@ private val logger = KotlinLogging.logger {} * Options for configuring the MCP client. * * @property capabilities The capabilities this client supports. - * @property enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with the server. + * @param enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with the server. */ public class ClientOptions( public val capabilities: ClientCapabilities = ClientCapabilities(), @@ -125,7 +125,6 @@ public suspend fun mcpClient( * @param clientInfo Information about the client implementation (name, version). * @param options Configuration options for this client. */ -@Suppress("TooManyFunctions") public open class Client(private val clientInfo: Implementation, options: ClientOptions = ClientOptions()) : Protocol(options) { @@ -242,7 +241,7 @@ public open class Client(private val clientInfo: Implementation, options: Client Method.Defined.CompletionComplete, -> { checkNotNull(serverCapabilities?.prompts) { - throw IllegalStateException("Server does not support prompts (required for $method)") + "Server does not support prompts (required for $method)" } } @@ -255,10 +254,10 @@ public open class Client(private val clientInfo: Implementation, options: Client val resCaps = serverCapabilities?.resources ?: error("Server does not support resources (required for $method)") - if (method == Method.Defined.ResourcesSubscribe && resCaps.subscribe != true) { - throw IllegalStateException( - "Server does not support resource subscriptions (required for $method)", - ) + if (method == Method.Defined.ResourcesSubscribe) { + check(resCaps.subscribe == true) { + "Server does not support resource subscriptions (required for $method)" + } } } @@ -556,9 +555,8 @@ public open class Client(private val clientInfo: Implementation, options: Client * @throws IllegalStateException If the client does not support roots. */ public fun removeRoot(uri: String): Boolean { - if (capabilities.roots == null) { - logger.error { "Failed to remove root '$uri': Client does not support roots capability" } - throw IllegalStateException("Client does not support roots capability.") + checkNotNull(capabilities.roots) { + "Client does not support roots capability." } logger.info { "Removing root: $uri" } val oldMap = roots.getAndUpdate { current -> current.remove(uri) } @@ -636,7 +634,6 @@ public open class Client(private val clientInfo: Implementation, options: Client // --- Internal Handlers --- - @Suppress("ReturnCount") private fun applyElicitationDefaults(request: ElicitRequest, result: ElicitResult): ElicitResult { if (result.action != ElicitResult.Action.Accept) return result val formParams = request.params as? ElicitRequestFormParams ?: return result diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt index a61ea4bf3..63f681c5c 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt @@ -227,7 +227,6 @@ public class StdioClientTransport @JvmOverloads public constructor( } override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { - @Suppress("SwallowedException") try { sendChannel.send(message) } catch (e: CancellationException) { @@ -290,7 +289,7 @@ public class StdioClientTransport @JvmOverloads public constructor( ) { val buffer = Buffer() try { - source.use { source -> + source.use { while (isActive) { val bytesRead = source.readAtMostTo(buffer, BUFFER_SIZE) if (bytesRead == -1L) { diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt index 999e25acb..c27ed79e1 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt @@ -54,6 +54,7 @@ private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID" * Represents an error from the Streamable HTTP transport. * * @property code HTTP status code associated with the error, or `null` if unavailable + * @param message detailed error description appended to the exception message */ public class StreamableHttpError(public val code: Int? = null, message: String? = null) : Exception("Streamable HTTP error: $message") @@ -65,14 +66,17 @@ private sealed interface ConnectResult { } /** - * Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. - * It will connect to a server using HTTP POST for sending messages and HTTP GET with Server-Sent Events - * for receiving messages. + * Client transport implementing the MCP Streamable HTTP transport specification. * - * @property sessionId session identifier assigned by the server after initialization, or `null` before connection - * @property protocolVersion MCP protocol version negotiated with the server, or `null` before connection + * Sends messages via HTTP POST and receives messages via HTTP GET with Server-Sent Events. + * Supports automatic SSE reconnection with exponential backoff, stream resumption via the + * `Last-Event-ID` header, and explicit session termination. + * + * @param client Ktor HTTP client used for all requests + * @param url MCP endpoint URL + * @param reconnectionOptions reconnection backoff and retry-limit settings for the SSE stream + * @param requestBuilder builder applied to every outgoing HTTP request, e.g. for adding auth headers */ -@Suppress("TooManyFunctions") public class StreamableHttpClientTransport( private val client: HttpClient, private val url: String, @@ -98,8 +102,11 @@ public class StreamableHttpClientTransport( override val logger: KLogger = KotlinLogging.logger {} + /** Session identifier assigned by the server after initialization, or `null` before connection. */ public var sessionId: String? = null private set + + /** MCP protocol version negotiated with the server, or `null` before connection. */ public var protocolVersion: String? = null private var sseJob: Job? = null @@ -121,7 +128,6 @@ public class StreamableHttpClientTransport( /** * Sends a single message with optional resumption support */ - @Suppress("ReturnCount", "CyclomaticComplexMethod", "LongMethod", "TooGenericExceptionCaught", "ThrowsCount") override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { logger.debug { "Client sending message via POST to $url: ${McpJson.encodeToString(message)}" } @@ -260,7 +266,6 @@ public class StreamableHttpClientTransport( var attempt = 0 var needsDelay = initialServerRetryDelay != null - @Suppress("LoopWithTooManyJumpStatements") while (isActive) { // Delay before (re)connection: skip only for first fresh SSE connection if (needsDelay) { @@ -296,7 +301,6 @@ public class StreamableHttpClientTransport( } } - @Suppress("TooGenericExceptionCaught") private suspend fun connectSse(lastEventId: String?): ConnectResult { logger.debug { "Client attempting to start SSE session at url: $url" } return try { @@ -363,7 +367,6 @@ public class StreamableHttpClientTransport( } } - @Suppress("TooGenericExceptionCaught") private suspend fun collectSse( session: ClientSSESession, replayMessageId: RequestId?, @@ -408,7 +411,6 @@ public class StreamableHttpClientTransport( return SseStreamResult(hasPrimingEvent, receivedResponse, localLastEventId, localServerRetryDelay) } - @Suppress("CyclomaticComplexMethod") private suspend fun handleInlineSse( response: HttpResponse, replayMessageId: RequestId?, @@ -455,7 +457,6 @@ public class StreamableHttpClientTransport( } } - @Suppress("LoopWithTooManyJumpStatements") while (!channel.isClosedForRead) { val line = channel.readUTF8Line() ?: break if (line.isEmpty()) { diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt index 67b2f07fb..8810552a8 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt @@ -834,7 +834,6 @@ class StreamableHttpClientTransportTest { transport.close() } - @Suppress("DEPRECATION") @Test fun testDeprecatedConstructorStillWorks() = runTest { val mockEngine = MockEngine { _ -> diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt index fb719041c..de9d2b3b5 100644 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt @@ -39,7 +39,6 @@ internal class MockMcp(verbose: Boolean = false) : AutoCloseable { val url = "${mokksy.baseUrl()}/mcp" - @Suppress("LongParameterList") fun onInitialize( clientName: String? = null, sessionId: String, @@ -117,7 +116,6 @@ internal class MockMcp(verbose: Boolean = false) : AutoCloseable { } } - @Suppress("LongParameterList") fun handleWithResult( httpMethod: HttpMethod = HttpMethod.Post, jsonRpcMethod: String, @@ -153,7 +151,6 @@ internal class MockMcp(verbose: Boolean = false) : AutoCloseable { } } - @Suppress("LongParameterList") fun handleWithResult( httpMethod: HttpMethod = HttpMethod.Post, jsonRpcMethod: String, @@ -178,7 +175,6 @@ internal class MockMcp(verbose: Boolean = false) : AutoCloseable { ) } - @Suppress("LongParameterList") fun handleJSONRPCRequest( httpMethod: HttpMethod = HttpMethod.Post, jsonRpcMethod: String, diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt index a1fe89e66..43be8c93e 100644 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt @@ -118,16 +118,16 @@ internal class StreamableHttpClientTest : AbstractStreamableHttpClientTest() { ServerSentEvent( event = "message", id = "1", - data = @Suppress("MaxLineLength") //language=json + data = """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""", ), ) delay(200.milliseconds) emit( ServerSentEvent( - data = @Suppress("MaxLineLength") //language=json + data = """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""", ), ) diff --git a/kotlin-sdk-core/build.gradle.kts b/kotlin-sdk-core/build.gradle.kts index 967dae315..1d19f5d3e 100644 --- a/kotlin-sdk-core/build.gradle.kts +++ b/kotlin-sdk-core/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("mcp.multiplatform") + id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) diff --git a/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml deleted file mode 100644 index d774a5bc4..000000000 --- a/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - CyclomaticComplexMethod:jsonUtils.kt:@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) private fun convertToJsonElement: JsonElement - LongMethod:Protocol.kt:Protocol$public suspend fun <T : RequestResult> request: T - MagicNumber:completion.kt:CompleteResult.Completion$100 - MatchingDeclarationName:capabilities.dsl.kt:ClientCapabilitiesBuilder - MatchingDeclarationName:methods.kt:Method - MaxLineLength:AbstractClientTransport.kt:AbstractClientTransport$* - MaxLineLength:ClientTransportState.kt:ClientTransportState.Initializing$* - MaxLineLength:Protocol.kt:Protocol$"Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}" - MaxLineLength:Protocol.kt:Protocol$* - MaxLineLength:Protocol.kt:RequestOptions$* - MaxLineLength:Transport.kt:Transport$* - MaxLineLength:TransportSendOptions.kt:TransportSendOptions$* - ReturnCount:ReadBuffer.kt:ReadBuffer$public fun readMessage: JSONRPCMessage? - ThrowsCount:AbstractClientTransport.kt:AbstractClientTransport$public override suspend fun send - TooGenericExceptionCaught:AbstractClientTransport.kt:AbstractClientTransport$e: Exception - TooGenericExceptionCaught:Protocol.kt:Protocol$cause: Throwable - TooGenericExceptionCaught:Protocol.kt:Protocol$error: Throwable - TooGenericExceptionCaught:Protocol.kt:Protocol$sendError: Throwable - TooGenericExceptionCaught:ReadBuffer.kt:ReadBuffer$e: Exception - TooGenericExceptionCaught:WebSocketMcpTransport.kt:WebSocketMcpTransport$e: Exception - TooManyFunctions:Protocol.kt:Protocol - - diff --git a/kotlin-sdk-core/detekt-baseline-main.xml b/kotlin-sdk-core/detekt-baseline-main.xml index a22fbcdfb..d483d7eda 100644 --- a/kotlin-sdk-core/detekt-baseline-main.xml +++ b/kotlin-sdk-core/detekt-baseline-main.xml @@ -2,31 +2,6 @@ - CyclomaticComplexMethod:jsonUtils.kt:@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) private fun convertToJsonElement: JsonElement - InjectDispatcher:utils.jvm.kt:IO - InstanceOfCheckForException:AbstractClientTransport.kt:AbstractClientTransport$e is McpException - LongMethod:Protocol.kt:Protocol$public suspend fun <T : RequestResult> request: T - MagicNumber:completion.kt:CompleteResult.Completion$100 - MatchingDeclarationName:capabilities.dsl.kt:ClientCapabilitiesBuilder - MatchingDeclarationName:methods.kt:Method - MaxLineLength:AbstractClientTransport.kt:AbstractClientTransport$* - MaxLineLength:ClientTransportState.kt:ClientTransportState.Initializing$* - MaxLineLength:Protocol.kt:Protocol$"Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}" - MaxLineLength:Protocol.kt:Protocol$* - MaxLineLength:Protocol.kt:RequestOptions$* - MaxLineLength:Transport.kt:Transport$* - MaxLineLength:TransportSendOptions.kt:TransportSendOptions$* - NoNameShadowing:Protocol.kt:Protocol$error - ReturnCount:ReadBuffer.kt:ReadBuffer$public fun readMessage: JSONRPCMessage? - ThrowsCount:AbstractClientTransport.kt:AbstractClientTransport$public override suspend fun send - TooGenericExceptionCaught:AbstractClientTransport.kt:AbstractClientTransport$e: Exception - TooGenericExceptionCaught:Protocol.kt:Protocol$cause: Throwable - TooGenericExceptionCaught:Protocol.kt:Protocol$error: Throwable - TooGenericExceptionCaught:Protocol.kt:Protocol$sendError: Throwable - TooGenericExceptionCaught:ReadBuffer.kt:ReadBuffer$e: Exception - TooGenericExceptionCaught:WebSocketMcpTransport.kt:WebSocketMcpTransport$e: Exception - TooManyFunctions:Protocol.kt:Protocol UnsafeCallOnNullableType:Protocol.kt:Protocol$response!! - UseCheckNotNull:Protocol.kt:Protocol$check(error != null) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt index 5f679992e..27013d3b2 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt @@ -99,8 +99,10 @@ public abstract class AbstractClientTransport : AbstractTransport() { * proceeding with further operations. It is often used to enforce state invariants within the transport's * lifecycle mechanics, enabling robust error handling and debugging in case of unexpected state transitions. * - * @param expected The expected transport state. An exception will be thrown if the actual state does not match this value. - * @throws IllegalStateException If the current state does not match the expected state. + * @param expected The expected transport state. + * An exception will be thrown if the actual state does not match this value. + * @param lazyMessage Function that builds the exception message from the actual state when the check fails. + * Defaults to a message that compares the expected state against the observed one. */ protected fun checkState( expected: ClientTransportState, diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt index 7b881004e..a73828979 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt @@ -6,11 +6,13 @@ import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi /** - * Implements [onClose], [onError] and [onMessage] functions of [Transport] providing - * corresponding [_onClose], [_onError] and [_onMessage] properties to use for an implementation. + * Base class for [Transport] implementations that handles the bookkeeping for [onClose], [onError] + * and [onMessage] callback registration, exposing the composed callbacks to subclasses for invocation. + * + * The callbacks are composed: each call to [onClose], [onError], or [onMessage] chains the new block + * after any previously registered ones. */ @OptIn(ExperimentalAtomicApi::class) -@Suppress("PropertyName") public abstract class AbstractTransport : Transport { private val onCloseCalled = AtomicBoolean(false) private var _onClose: (() -> Unit) = {} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 75cebd3dd..8f86c035f 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -53,22 +53,16 @@ public typealias ProgressCallback = (Progress) -> Unit /** * Additional initialization options. * + * @property enforceStrictCapabilities whether to restrict emitted requests to only those that the + * remote side has indicated it can handle through its advertised capabilities. + * This does NOT affect checking of _local_ side capabilities, as it is considered a logic error + * to mis-specify those. + * Currently defaults to `false` for backwards compatibility with SDK versions that did not advertise + * capabilities correctly; in the future, this will default to `true`. * @property timeout default timeout for outgoing requests */ public open class ProtocolOptions( - /** - * Whether to restrict emitted requests to only those that the remote side has indicated - * that they can handle, through their advertised capabilities. - * - * Note that this DOES NOT affect checking of _local_ side capabilities, as it is - * considered a logic error to mis-specify those. - * - * Currently, this defaults to false, for backwards compatibility with SDK versions - * that did not advertise capabilities correctly. - * In the future, this will default to true. - */ public var enforceStrictCapabilities: Boolean = false, - public var timeout: Duration = DEFAULT_REQUEST_TIMEOUT, ) @@ -80,18 +74,18 @@ public val DEFAULT_REQUEST_TIMEOUT: Duration = 60.seconds /** * Options that can be given per request. * - * @property relatedRequestId if present, - * `relatedRequestId` is used to indicate to the transport which incoming request to associate this outgoing message with. - * @property resumptionToken the resumption token used to continue long-running requests that were interrupted. - * This allows clients to reconnect and continue from where they left off, if supported by the transport. - * @property onResumptionToken a callback that is invoked when the resumption token changes, if supported by the transport. - * This allows clients to persist the latest token for potential reconnection. + * @param relatedRequestId if present, used to indicate to the transport which incoming request to + * associate this outgoing message with. + * @param resumptionToken the resumption token used to continue long-running requests that were interrupted. + * Allows clients to reconnect and continue from where they left off, if supported by the transport. + * @param onResumptionToken callback invoked when the resumption token changes, if supported by the transport. + * Allows clients to persist the latest token for potential reconnection. * @property onProgress callback for progress notifications. - * If set, requests progress notifications from the remote end (if supported). - * When progress notifications are received, this callback will be invoked. + * If set, requests progress notifications from the remote end (if supported); + * when progress notifications are received, this callback is invoked. * @property timeout a timeout for this request. - * If exceeded, a McpException with code `RequestTimeout` will be raised from request(). - * If not specified, `DEFAULT_REQUEST_TIMEOUT` will be used as the timeout. + * If exceeded, a [McpException] with code `RequestTimeout` is raised from [Protocol.request]. + * If not specified, [DEFAULT_REQUEST_TIMEOUT] is used. */ public class RequestOptions( relatedRequestId: RequestId? = null, @@ -147,19 +141,18 @@ internal val COMPLETED = CompletableDeferred(Unit).also { it.complete(Unit) } * Implements MCP protocol framing on top of a pluggable transport, including * features like request/response linking, notifications, and progress. * - * @property transport the active transport, or `null` if not connected - * @property requestHandlers registered request handlers keyed by method name - * @property notificationHandlers registered notification handlers keyed by method name - * @property responseHandlers pending response handlers keyed by request ID - * @property progressHandlers registered progress callbacks keyed by progress token + * @property options protocol-level configuration; `null` falls back to defaults */ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptions?) { + /** The active transport, or `null` if not connected. */ public var transport: Transport? = null private set private val _requestHandlers: AtomicRef RequestResult?>> = atomic(persistentMapOf()) + + /** Registered request handlers keyed by method name. */ public val requestHandlers: Map< String, suspend ( @@ -171,17 +164,23 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio private val _notificationHandlers = atomic(persistentMapOf Unit>()) + + /** Registered notification handlers keyed by method name. */ public val notificationHandlers: Map Unit> get() = _notificationHandlers.value private val _responseHandlers: AtomicRef Unit>> = atomic(persistentMapOf()) + + /** Pending response handlers keyed by request ID. */ public val responseHandlers: Map Unit> get() = _responseHandlers.value private val _progressHandlers: AtomicRef> = atomic(persistentMapOf()) + + /** Registered progress callbacks keyed by progress token. */ public val progressHandlers: Map get() = _progressHandlers.value @@ -285,7 +284,6 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } } - @Suppress("ThrowsCount") private suspend fun onRequest(request: JSONRPCRequest) { logger.trace { "Received request: ${request.method} (id: ${request.id})" } @@ -312,7 +310,6 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio return } - @Suppress("TooGenericExceptionCaught", "InstanceOfCheckForException") try { val result = handler(request, RequestHandlerExtra()) logger.trace { "Request handled successfully: ${request.method} (id: ${request.id})" } @@ -395,7 +392,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio if (response != null) { handler(response, null) } else { - check(error != null) + checkNotNull(error) val mcpException = McpException( code = error.error.code, message = error.error.message, @@ -479,8 +476,8 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio try { @Suppress("UNCHECKED_CAST") result.complete(response!!.result as T) - } catch (error: Throwable) { - result.completeExceptionally(error) + } catch (e: Throwable) { + result.completeExceptionally(e) } } } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt index d4c8b4347..c0c853f62 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt @@ -39,7 +39,6 @@ public class ReadBuffer { val line = readNextLine() ?: return null if (line.isBlank()) continue - @Suppress("TooGenericExceptionCaught") val message = try { deserializeMessage(line) } catch (e: Exception) { @@ -78,7 +77,7 @@ public class ReadBuffer { if (braceIndex == -1) return null val trimmed = line.substring(braceIndex) - @Suppress("TooGenericExceptionCaught") + return try { deserializeMessage(trimmed) } catch (ignored: Exception) { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt index cb57547ce..a89c10291 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt @@ -21,9 +21,9 @@ public interface Transport { /** * Sends a JSON-RPC message (request or response). * - * @property message The JSON-RPC message to send, either a request or a response. - * @property options Optional transport-specific options that control sending behavior. - * Different transport implementations may support different options. + * @param message the JSON-RPC message to send, either a request or a response + * @param options optional transport-specific options that control sending behavior; + * different transport implementations may support different options */ public suspend fun send(message: JSONRPCMessage, options: TransportSendOptions? = null) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt index 9fa1a0ba0..baf37233e 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt @@ -45,7 +45,6 @@ public abstract class WebSocketMcpTransport : AbstractTransport() { */ protected abstract suspend fun initializeSession() - @Suppress("ThrowsCount") override suspend fun start() { logger.debug { "Starting websocket transport" } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt index 6b1249274..56c851bfb 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt @@ -6,11 +6,11 @@ import kotlin.jvm.JvmOverloads /** * Represents an error specific to the MCP protocol. * - * @property code The MCP/JSON‑RPC error code. - * @property data Optional additional error payload as a JSON element; `null` when not provided. - * @param message The error message. Used verbatim as [Exception.message] — no error code prefix is prepended. - * Defaults to `"MCP error $code"` when not provided. - * @param cause The original cause. + * @property code the MCP/JSON-RPC error code + * @param message the error message; used verbatim as [Exception.message] without an error-code prefix. + * Defaults to `"MCP error $code"` when not provided. + * @property data optional additional error payload as a JSON element + * @param cause the original cause */ public class McpException @JvmOverloads public constructor( public val code: Int, diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt index c45ed301b..8280404df 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable /** * Represents a ping request used to check if the connection is alive. * - * @property meta optional request metadata + * @property params optional request parameters carrying metadata */ @Serializable public data class PingRequest(override val params: BaseRequestParams? = null) : @@ -17,6 +17,7 @@ public data class PingRequest(override val params: BaseRequestParams? = null) : @EncodeDefault override val method: Method = Method.Defined.Ping + /** Convenience accessor for [params]'s metadata. */ public val meta: RequestMeta? get() = params?.meta } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt index ca083568e..f65b1eddb 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt @@ -28,11 +28,10 @@ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( /** * Represents an entity that includes additional metadata in its responses. - * - * @property meta optional metadata attached to this entity */ @Serializable public sealed interface WithMeta { + /** Optional metadata attached to this entity. */ @SerialName("_meta") public val meta: JsonObject? } @@ -91,9 +90,6 @@ public data class Icon( ) { /** * The theme context for which an icon is designed. - * - * @property Light Icon designed for use with a light background (typically darker icon). - * @property Dark Icon designed for use with a dark background (typically lighter icon). */ @Serializable public enum class Theme { @@ -128,11 +124,10 @@ public enum class Role { * * References are used to point to other entities (prompts, resources, etc.) * without including their full definitions. - * - * @property type discriminator identifying the reference subtype */ @Serializable(with = ReferencePolymorphicSerializer::class) public sealed interface Reference { + /** Discriminator identifying the reference subtype. */ public val type: ReferenceType } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt index c782e3e7d..1eb532914 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt @@ -12,7 +12,6 @@ import kotlinx.serialization.json.JsonObject * A request from the client to the server to ask for completion options. * * @property params The request parameters containing the argument to complete and its context. - * @property meta optional request metadata */ @Serializable public data class CompleteRequest(override val params: CompleteRequestParams) : ClientRequest { @@ -38,6 +37,7 @@ public data class CompleteRequest(override val params: CompleteRequestParams) : public val context: Context? get() = params.context + /** Convenience accessor for [params]'s metadata. */ public val meta: RequestMeta? get() = params.meta } @@ -48,6 +48,8 @@ public data class CompleteRequest(override val params: CompleteRequestParams) : * @property argument The argument's information for which completion options are requested. * @property ref A reference to either a prompt or resource template to complete within. * @property context Additional, optional context for generating completions. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. */ @Serializable public data class CompleteRequestParams( diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt index d30c22e40..815e5f631 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt @@ -39,11 +39,10 @@ public enum class ContentTypes(public val value: String) { /** * Base interface for all content blocks in the protocol. - * - * @property type discriminator identifying the content block subtype */ @Serializable(with = ContentBlockPolymorphicSerializer::class) public sealed interface ContentBlock : WithMeta { + /** Discriminator identifying the content block subtype. */ public val type: ContentTypes } @@ -63,11 +62,10 @@ public sealed interface MediaContent : * * Implemented by [TextContent], [ImageContent], [AudioContent], [ToolUseContent], * and [ToolResultContent]. - * - * @property type discriminator identifying the content block subtype */ @Serializable(with = SamplingMessageContentPolymorphicSerializer::class) public sealed interface SamplingMessageContent : WithMeta { + /** discriminator identifying the content block subtype */ public val type: ContentTypes } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt index bb303f0c6..a0ee74c9a 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt @@ -46,11 +46,10 @@ public data class ElicitRequest(override val params: ElicitRequestParams) : Serv * Represents the parameters for an `elicitation/create` request. * * Implementations: [ElicitRequestFormParams], [ElicitRequestURLParams]. - * - * @property message The message to present to the user describing what information is being requested. */ @Serializable(with = ElicitRequestParamsSerializer::class) public sealed interface ElicitRequestParams : RequestParams { + /** The message to present to the user describing what information is being requested. */ public val message: String /** @@ -64,7 +63,6 @@ public sealed interface ElicitRequestParams : RequestParams { * Each property must be a primitive type (string, number, boolean). * @property required Optional list of property names that must be provided by the user. * If omitted, all fields are considered optional. - * @property type Always "object" for elicitation schemas. */ @Serializable public data class RequestedSchema( @@ -73,6 +71,7 @@ public sealed interface ElicitRequestParams : RequestParams { val properties: Map, val required: List? = null, ) { + /** Always "object" for elicitation schemas. */ @EncodeDefault val type: String = "object" } @@ -91,7 +90,7 @@ public sealed interface ElicitRequestParams : RequestParams { ReplaceWith("ElicitRequestFormParams(message, requestedSchema = requestedSchema, meta = meta)"), DeprecationLevel.WARNING, ) -@Suppress("FunctionNaming", "FunctionName") +@Suppress("FunctionName") public fun ElicitRequestParams( message: String, requestedSchema: ElicitRequestParams.RequestedSchema, @@ -113,7 +112,6 @@ public fun ElicitRequestParams( * later via `tasks/result`. * @property requestedSchema A restricted subset of JSON Schema. Only top-level properties * are allowed, without nesting. - * @property mode The elicitation mode discriminator, always `"form"`. * @property meta Optional metadata. May include a progressToken for out-of-band progress notifications. */ @Serializable @@ -124,6 +122,7 @@ public data class ElicitRequestFormParams( @SerialName("_meta") override val meta: RequestMeta? = null, ) : ElicitRequestParams { + /** The elicitation mode discriminator, always `"form"`. */ @EncodeDefault public val mode: String = "form" } @@ -141,7 +140,6 @@ public data class ElicitRequestFormParams( * @property task If specified, the caller is requesting task-augmented execution. The request * will return a [CreateTaskResult] immediately, and the actual result can be retrieved * later via `tasks/result`. - * @property mode The elicitation mode discriminator, always `"url"`. * @property meta Optional metadata. May include a progressToken for out-of-band progress notifications. */ @Serializable @@ -153,6 +151,7 @@ public data class ElicitRequestURLParams( @SerialName("_meta") override val meta: RequestMeta? = null, ) : ElicitRequestParams { + /** The elicitation mode discriminator, always `"url"`. */ @EncodeDefault public val mode: String = "url" } @@ -182,10 +181,6 @@ public data class ElicitResult( /** * The user's response action to an elicitation request. - * - * @property Accept User submitted the form/confirmed the action. Content will be provided. - * @property Decline User explicitly declined the action. No content provided. - * @property Cancel User dismissed the dialog without making an explicit choice. No content provided. */ @Serializable public enum class Action { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt index 1a9ea2eca..7eefdb500 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt @@ -104,11 +104,10 @@ internal fun JSONRPCNotification.fromJSON(): Notification = * Base interface for all JSON-RPC 2.0 messages. * * All messages in the MCP protocol follow the JSON-RPC 2.0 specification. - * - * @property jsonrpc the JSON-RPC protocol version, always `"2.0"` */ @Serializable(with = JSONRPCMessagePolymorphicSerializer::class) public sealed interface JSONRPCMessage { + /** The JSON-RPC protocol version, always `"2.0"`. */ public val jsonrpc: String } @@ -131,7 +130,6 @@ public data object JSONRPCEmptyMessage : JSONRPCMessage { * The server or client (depending on a direction) must respond with either a * [JSONRPCResponse] or [JSONRPCError] that has the same [id]. * - * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @property id A unique identifier for this request. The response will include the same ID. * Can be a string or number. UUID string is used by default. * @property method The name of the method to invoke (e.g., "tools/list", "resources/read"). @@ -144,6 +142,7 @@ public data class JSONRPCRequest @JvmOverloads public constructor( val method: String, val params: JsonElement? = null, ) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ @EncodeDefault override val jsonrpc: String = JSONRPC_VERSION @@ -186,13 +185,13 @@ public data class JSONRPCRequest @JvmOverloads public constructor( * * Examples: progress updates, resource change notifications, log messages. * - * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @property method The name of the notification method (e.g., "notifications/progress", * "notifications/resources/updated"). * @property params Optional parameters for the notification. Structure depends on the specific method. */ @Serializable public data class JSONRPCNotification(val method: String, val params: JsonElement? = null) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ @EncodeDefault override val jsonrpc: String = JSONRPC_VERSION } @@ -207,12 +206,12 @@ public data class JSONRPCNotification(val method: String, val params: JsonElemen * Sent in response to a [JSONRPCRequest] when the method execution succeeds. * The [id] must match the [id] of the original request. * - * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @property id The identifier from the original request. Used to match responses to requests. * @property result The result of the method execution. Structure depends on the method that was called. */ @Serializable public data class JSONRPCResponse(val id: RequestId, val result: RequestResult = EmptyResult()) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ @EncodeDefault override val jsonrpc: String = JSONRPC_VERSION } @@ -227,12 +226,12 @@ public data class JSONRPCResponse(val id: RequestId, val result: RequestResult = * Sent in response to a [JSONRPCRequest] when the method execution fails. * The [id] must match the [id] of the original request. * - * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @property id The identifier from the original request. Used to match error responses to requests. * @property error Details about the error that occurred, including error code and message. */ @Serializable public data class JSONRPCError(val id: RequestId?, val error: RPCError) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ @EncodeDefault override val jsonrpc: String = JSONRPC_VERSION } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt index dae2fc742..19f4da544 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt @@ -4,11 +4,10 @@ import kotlinx.serialization.Serializable /** * Represents a method in the protocol, which can be predefined or custom. - * - * @property value the string representation of this method name */ @Serializable(with = MethodSerializer::class) public sealed interface Method { + /** The string representation of this method name. */ public val value: String /** diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt index 24be040c9..3d33c2132 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt @@ -11,13 +11,13 @@ import kotlinx.serialization.json.JsonObject /** * Represents a notification in the protocol. - * - * @property method the notification method identifier - * @property params optional notification parameters */ @Serializable(with = NotificationPolymorphicSerializer::class) public sealed interface Notification { + /** The notification method identifier. */ public val method: Method + + /** Optional notification parameters. */ public val params: NotificationParams? } @@ -34,9 +34,7 @@ public sealed interface ClientNotification : Notification public sealed interface ServerNotification : Notification /** - * Interface for notification parameter types. - * - * @property meta Optional metadata for the notification. + * Interface for notification parameter types. Inherits the optional `_meta` field from [WithMeta]. */ @Serializable public sealed interface NotificationParams : WithMeta @@ -76,13 +74,13 @@ public class Progress( * @property method The custom method name. By convention, custom methods often contain * organization-specific prefixes (e.g., "mycompany/custom_event"). * @property params Raw JSON parameters for the custom notification, if present. - * @property meta optional metadata for this notification */ @Serializable public data class CustomNotification(override val method: Method, override val params: BaseNotificationParams? = null) : ClientNotification, ServerNotification { + /** Convenience accessor for [params]'s metadata. */ public val meta: JsonObject? get() = params?.meta } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt index 9f52ec9e8..aacab9a90 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt @@ -15,11 +15,11 @@ import kotlin.jvm.JvmInline * Metadata attached to a request's `_meta` field. * * @property json the raw JSON object containing the metadata - * @property progressToken optional progress token for tracking request progress */ @JvmInline @Serializable public value class RequestMeta(public val json: JsonObject) { + /** Optional progress token for tracking request progress. */ public val progressToken: ProgressToken? get() = json["progressToken"]?.let { element -> when { @@ -72,14 +72,14 @@ public data class BaseRequestParams(@SerialName("_meta") override val meta: Requ /** * Represents a request in the protocol. - * - * @property method the request method identifier - * @property params optional request parameters */ @OptIn(ExperimentalSerializationApi::class) @Serializable(with = RequestPolymorphicSerializer::class) public sealed interface Request { + /** The request method identifier. */ public val method: Method + + /** Optional request parameters. */ public val params: RequestParams? } @@ -142,7 +142,7 @@ public sealed interface ServerResult : RequestResult /** * An empty result for a request containing optional metadata. * - * @param meta Additional metadata for the response. Defaults to an empty JSON object. + * @property meta Additional metadata for the response. Defaults to an empty JSON object. */ @Serializable public data class EmptyResult(@SerialName("_meta") override val meta: JsonObject? = null) : @@ -151,11 +151,10 @@ public data class EmptyResult(@SerialName("_meta") override val meta: JsonObject /** * Represents a paginated result. - * - * @property nextCursor opaque token for retrieving the next page of results, or `null` if no more results */ @Serializable public sealed interface PaginatedResult : RequestResult { + /** Opaque token for retrieving the next page of results, or `null` if no more results. */ public val nextCursor: String? } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt index 86ce3cee7..1afa0e69a 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt @@ -117,15 +117,14 @@ public data class ResourceTemplateReference(val uri: String) : Reference { } /** - * The contents of a specific resource or sub-resource. - * - * @property uri The URI of this resource. - * @property mimeType The MIME type of this resource, if known. - * @property meta Optional metadata for this response. + * The contents of a specific resource or sub-resource. Carries the optional `_meta` field via [WithMeta]. */ @Serializable(with = ResourceContentsPolymorphicSerializer::class) public sealed interface ResourceContents : WithMeta { + /** The URI of this resource. */ public val uri: String + + /** The MIME type of this resource, if known. */ public val mimeType: String? } @@ -136,6 +135,7 @@ public sealed interface ResourceContents : WithMeta { * This must only be set if the item can actually be represented as text (not binary data). * @property uri The URI of this resource. * @property mimeType The MIME type of this resource, if known. + * @property meta Optional metadata for these contents. */ @Serializable public data class TextResourceContents( @@ -152,6 +152,7 @@ public data class TextResourceContents( * @property blob A base64-encoded string representing the binary data of the item. * @property uri The URI of this resource. * @property mimeType The MIME type of this resource, if known. + * @property meta Optional metadata for these contents. */ @Serializable public data class BlobResourceContents( @@ -167,6 +168,7 @@ public data class BlobResourceContents( * * @property uri The URI of this resource. * @property mimeType The MIME type of this resource, if known. + * @property meta Optional metadata for these contents. */ @Serializable public data class UnknownResourceContents( diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt index 194ea4fb2..7ee4f9e54 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt @@ -53,7 +53,6 @@ public data class Root( * `listChanged = true` to receive these requests. * * @property params Optional request parameters containing metadata. - * @property meta optional request metadata */ @Serializable public data class ListRootsRequest(override val params: BaseRequestParams? = null) : ServerRequest { @@ -61,6 +60,7 @@ public data class ListRootsRequest(override val params: BaseRequestParams? = nul @EncodeDefault override val method: Method = Method.Defined.RootsList + /** Convenience accessor for [params]'s metadata. */ public val meta: RequestMeta? get() = params?.meta } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt index 479cc66f0..098250073 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt @@ -24,7 +24,6 @@ public sealed interface PrimitiveSchemaDefinition * @property minLength Minimum string length. * @property maxLength Maximum string length. * @property format Optional format constraint (e.g., email, URI, date). - * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Serializable @@ -36,6 +35,7 @@ public data class StringSchema( val format: StringSchemaFormat? = null, val default: String? = null, ) : PrimitiveSchemaDefinition { + /** JSON Schema type discriminator, always `"string"`. */ @EncodeDefault public val type: String = "string" } @@ -73,7 +73,6 @@ public sealed interface NumberSchemaDefinition : PrimitiveSchemaDefinition * @property description Optional description for the field. * @property minimum Minimum allowed value. * @property maximum Maximum allowed value. - * @property type JSON Schema type discriminator, always `"integer"`. * @property default Optional default value. */ @Serializable @@ -84,6 +83,7 @@ public data class IntegerSchema( val maximum: Int? = null, val default: Int? = null, ) : NumberSchemaDefinition { + /** JSON Schema type discriminator, always `"integer"`. */ @EncodeDefault val type: String = "integer" } @@ -95,7 +95,6 @@ public data class IntegerSchema( * @property description Optional description for the field. * @property minimum Minimum allowed value. * @property maximum Maximum allowed value. - * @property type JSON Schema type discriminator, always `"number"`. * @property default Optional default value. */ @Serializable @@ -106,6 +105,7 @@ public data class DoubleSchema( val maximum: Double? = null, val default: Double? = null, ) : NumberSchemaDefinition { + /** JSON Schema type discriminator, always `"number"`. */ @EncodeDefault val type: String = "number" } @@ -115,7 +115,6 @@ public data class DoubleSchema( * * @property title Optional display title for the field. * @property description Optional description for the field. - * @property type JSON Schema type discriminator, always `"boolean"`. * @property default Optional default value. */ @Serializable @@ -124,6 +123,7 @@ public data class BooleanSchema( val description: String? = null, val default: Boolean? = null, ) : PrimitiveSchemaDefinition { + /** JSON Schema type discriminator, always `"boolean"`. */ @EncodeDefault val type: String = "boolean" } @@ -161,7 +161,6 @@ public sealed interface SingleSelectEnumSchema : EnumSchemaDefinition * @property title Optional display title for the field. * @property description Optional description for the field. * @property enumValues Array of enum values to choose from. - * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Serializable @@ -172,6 +171,7 @@ public data class UntitledSingleSelectEnumSchema( val enumValues: List, val default: String? = null, ) : SingleSelectEnumSchema { + /** JSON Schema type discriminator, always `"string"`. */ @EncodeDefault val type: String = "string" } @@ -182,7 +182,6 @@ public data class UntitledSingleSelectEnumSchema( * @property title Optional display title for the field. * @property description Optional description for the field. * @property oneOf Array of enum options with values and display labels. - * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Serializable @@ -192,6 +191,7 @@ public data class TitledSingleSelectEnumSchema( val oneOf: List, val default: String? = null, ) : SingleSelectEnumSchema { + /** JSON Schema type discriminator, always `"string"`. */ @EncodeDefault val type: String = "string" } @@ -205,7 +205,6 @@ public data class TitledSingleSelectEnumSchema( * @property description Optional description for the field. * @property enumValues Array of enum values to choose from. * @property enumNames Display names for enum values. Non-standard according to JSON Schema 2020-12. - * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Deprecated("Use TitledSingleSelectEnumSchema instead") @@ -218,6 +217,7 @@ public data class LegacyTitledEnumSchema( val enumNames: List? = null, val default: String? = null, ) : EnumSchemaDefinition { + /** JSON Schema type discriminator, always `"string"`. */ @EncodeDefault val type: String = "string" } @@ -238,7 +238,6 @@ public sealed interface MultiSelectEnumSchema : EnumSchemaDefinition * @property minItems Minimum number of items to select. * @property maxItems Maximum number of items to select. * @property items Schema for the array items. - * @property type JSON Schema type discriminator, always `"array"`. * @property default Optional default value. */ @Serializable @@ -250,13 +249,13 @@ public data class UntitledMultiSelectEnumSchema( val items: Items, val default: List? = null, ) : MultiSelectEnumSchema { + /** JSON Schema type discriminator, always `"array"`. */ @EncodeDefault val type: String = "array" /** * Schema for the array items with plain enum values. * - * @property type JSON Schema type discriminator, always `"string"`. * @property enumValues Array of enum values to choose from. */ @Serializable @@ -264,6 +263,7 @@ public data class UntitledMultiSelectEnumSchema( @SerialName("enum") val enumValues: List, ) { + /** JSON Schema type discriminator, always `"string"`. */ @EncodeDefault val type: String = "string" } @@ -277,7 +277,6 @@ public data class UntitledMultiSelectEnumSchema( * @property minItems Minimum number of items to select. * @property maxItems Maximum number of items to select. * @property items Schema for array items with enum options and display labels. - * @property type JSON Schema type discriminator, always `"array"`. * @property default Optional default value. */ @Serializable @@ -289,6 +288,7 @@ public data class TitledMultiSelectEnumSchema( val items: Items, val default: List? = null, ) : MultiSelectEnumSchema { + /** JSON Schema type discriminator, always `"array"`. */ @EncodeDefault val type: String = "array" diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt index 4a1abc61b..ab85e6d87 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt @@ -424,7 +424,6 @@ private fun selectClientResultDeserializer(element: JsonElement): Deserializatio * Selects the appropriate deserializer for server results based on JSON content. * Returns null if the structure doesn't match any known server result type. */ -@Suppress("CyclomaticComplexMethod") private fun selectServerResultDeserializer(element: JsonElement): DeserializationStrategy? { val jsonObject = element.jsonObject return when { @@ -530,7 +529,6 @@ internal object RequestIdPolymorphicSerializer : JsonContentPolymorphicSerialize internal object ElicitRequestParamsSerializer : JsonContentPolymorphicSerializer( ElicitRequestParams::class, ) { - @Suppress("ThrowsCount") override fun selectDeserializer(element: JsonElement): DeserializationStrategy { val mode = when (val modeElement = element.jsonObject["mode"]) { null -> null @@ -559,7 +557,6 @@ internal object ElicitRequestParamsSerializer : JsonContentPolymorphicSerializer internal object PrimitiveSchemaDefinitionSerializer : JsonContentPolymorphicSerializer( PrimitiveSchemaDefinition::class, ) { - @Suppress("ThrowsCount") override fun selectDeserializer(element: JsonElement): DeserializationStrategy { val obj = element.jsonObject val typeElement = obj["type"] @@ -589,7 +586,6 @@ internal object PrimitiveSchemaDefinitionSerializer : JsonContentPolymorphicSeri } } - @Suppress("DEPRECATION") private fun selectStringTypeDeserializer( obj: Map, ): DeserializationStrategy = when { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt index 0f154e5c0..7363561ae 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt @@ -17,22 +17,27 @@ public const val RELATED_TASK_META_KEY: String = "io.modelcontextprotocol/relate /** * Common fields shared by all types representing the task state. - * - * @property taskId the task identifier - * @property status current task state - * @property statusMessage optional human-readable message describing the current task state - * @property createdAt ISO 8601 timestamp when the task was created - * @property lastUpdatedAt ISO 8601 timestamp when the task was last updated - * @property ttl actual retention duration from creation in milliseconds, or `null` for unlimited - * @property pollInterval suggested polling interval in milliseconds */ public sealed interface TaskFields { + /** The task identifier. */ public val taskId: String + + /** Current task state. */ public val status: TaskStatus + + /** Optional human-readable message describing the current task state. */ public val statusMessage: String? + + /** ISO 8601 timestamp when the task was created. */ public val createdAt: String + + /** ISO 8601 timestamp when the task was last updated. */ public val lastUpdatedAt: String + + /** Actual retention duration from creation in milliseconds, or `null` for unlimited. */ public val ttl: Long? + + /** Suggested polling interval in milliseconds. */ public val pollInterval: Long? } @@ -237,13 +242,13 @@ public data class GetTaskPayloadRequestParams( * For example, a tools/call task would return the [CallToolResult] structure. * * @property json the raw JSON object containing the result payload - * @property meta Optional metadata for this response. */ @JvmInline @Serializable public value class GetTaskPayloadResult(public val json: JsonObject) : ClientResult, ServerResult { + /** Optional metadata for this response, extracted from the `_meta` field of [json]. */ override val meta: JsonObject? get() = json["_meta"]?.takeIf { it is JsonObject } as? JsonObject diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt index a03e8ad2a..2ff890a1e 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt @@ -84,7 +84,6 @@ public data class Tool( * @property schema Optional URI identifying the JSON Schema dialect (e.g., * `https://json-schema.org/draft/2020-12/schema`). Serialized as `$schema`. * When absent, JSON Schema 2020-12 is assumed by default. - * @property type Always "object" for tool schemas. * @property properties Optional map of property names to their schema definitions. * @property required Optional list of property names that are required. * @property defs Optional schema definitions available to references in [properties]. Serialized as `$defs`. @@ -98,6 +97,7 @@ public data class ToolSchema( @SerialName("\$defs") val defs: JsonObject? = null, ) { + /** Always `"object"` for tool schemas. */ @OptIn(ExperimentalSerializationApi::class) @EncodeDefault val type: String = "object" diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt index c5619d597..aa2abb994 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt @@ -66,11 +66,9 @@ private const val VARIABLE_MATCH_SCORE = 1 * no normalizer is available, so dot-segment traversal is **not** mitigated at this * layer — handlers on those targets must normalize paths themselves. * - * @param resourceTemplate The resource template to match against. Must follow RFC 6570 Level 1 syntax. - * @param maxUriLength Maximum allowed length for incoming URIs. Defaults to 2048. - * @param maxDepth Maximum allowed segment count for the template/uri. Defaults to 50. - * - * @property resourceTemplate The resource template against which resource URIs will be matched. + * @property resourceTemplate the resource template to match against; must follow RFC 6570 Level 1 syntax + * @param maxDepth maximum allowed segment count for the template/uri (defaults to 50) + * @param maxUriLength maximum allowed length for incoming URIs (defaults to 2048) */ public class PathSegmentTemplateMatcher @JvmOverloads constructor( override val resourceTemplate: ResourceTemplate, @@ -118,7 +116,6 @@ public class PathSegmentTemplateMatcher @JvmOverloads constructor( variableIndices = vars.toImmutableMap() } - @Suppress("ReturnCount") override fun match(resourceUri: String): MatchResult? { if (resourceUri.length > maxUriLength) { logger.debug { "URL is too long (max=$maxUriLength)" } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt index 867cc9f1e..d99bfdac2 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt @@ -32,11 +32,10 @@ public class MatchResult(public val variables: Map, public val s * candidate URIs against that template via [match]. The returned [MatchResult.score] * must reflect match specificity so that a selection algorithm can prefer the most * specific template when multiple templates match the same URI. - * - * @property resourceTemplate the template to match URIs against */ public interface ResourceTemplateMatcher { + /** The template to match URIs against. */ public val resourceTemplate: ResourceTemplate /** diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt index 5392890df..1a418639a 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt @@ -176,7 +176,6 @@ class ElicitationTest { // ── Deprecated compat ─────────────────────────────────────────────── @Test - @Suppress("DEPRECATION") fun `deprecated factory function should create ElicitRequestFormParams`() { val schema = ElicitRequestParams.RequestedSchema( properties = mapOf("name" to StringSchema()), @@ -188,7 +187,6 @@ class ElicitationTest { } @Test - @Suppress("DEPRECATION") fun `deprecated requestedSchema should return schema for form mode`() { val schema = ElicitRequestParams.RequestedSchema( properties = mapOf("x" to BooleanSchema()), @@ -198,7 +196,6 @@ class ElicitationTest { } @Test - @Suppress("DEPRECATION") fun `deprecated requestedSchema should return null for URL mode`() { val request = ElicitRequest( ElicitRequestURLParams(message = "m", elicitationId = "id", url = "https://example.com"), diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt index ed97c18fd..e5797b9f8 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt @@ -15,7 +15,6 @@ import kotlin.test.assertNull class InitializeTest { @Test - @Suppress("LongMethod") fun `should serialize InitializeRequest with capabilities and meta`() { val request = InitializeRequest( InitializeRequestParams( diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt index 954fb1ef5..904454ff3 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt @@ -397,7 +397,6 @@ class PrimitiveSchemaTest { // ── LegacyTitledEnumSchema ──────────────────────────────────────────── - @Suppress("DEPRECATION") @Test fun `should serialize LegacyTitledEnumSchema`() { verifySerialization( @@ -420,7 +419,6 @@ class PrimitiveSchemaTest { ) } - @Suppress("DEPRECATION") @Test fun `should deserialize LegacyTitledEnumSchema from JSON`() { val json = """ diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt index 0432abac1..62a82cd4f 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt @@ -106,7 +106,6 @@ class SamplingTest { } @Test - @Suppress("LongMethod") fun `should serialize CreateMessageRequest with all fields`() { val request = CreateMessageRequest( CreateMessageRequestParams( diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt index 87f1644c7..d5e1d5a94 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt @@ -37,7 +37,6 @@ class ToolsTest { } @Test - @Suppress("LongMethod") fun `should serialize Tool with annotations and schemas`() { val inputSchema = ToolSchema( properties = buildJsonObject { diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt index 10be8b094..98817c11e 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt @@ -28,7 +28,6 @@ import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) class SamplingDslTest { @Test - @Suppress("LongMethod") fun `buildCreateMessageRequest should build with all fields`() { val request = buildCreateMessageRequest { maxTokens = 1000 diff --git a/kotlin-sdk-server/build.gradle.kts b/kotlin-sdk-server/build.gradle.kts index 126c51601..193eb909f 100644 --- a/kotlin-sdk-server/build.gradle.kts +++ b/kotlin-sdk-server/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("mcp.multiplatform") + id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) diff --git a/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml deleted file mode 100644 index d383ec9e8..000000000 --- a/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - MagicNumber:StdioServerTransport.kt:StdioServerTransport$8192 - MaxLineLength:SSEServerTransport.kt:SseServerTransport$"SSEServerTransport already started! If using Server class, note that connect() calls start() automatically." - MaxLineLength:SSEServerTransport.kt:SseServerTransport$* - ReturnCount:KtorServer.kt:private suspend fun existingStreamableTransport: StreamableHttpServerTransport? - TooGenericExceptionCaught:SSEServerTransport.kt:SseServerTransport$e: Exception - TooGenericExceptionCaught:StdioServerTransport.kt:StdioServerTransport$e: Throwable - TooManyFunctions:KtorServer.kt:io.modelcontextprotocol.kotlin.sdk.server.KtorServer.kt - - diff --git a/kotlin-sdk-server/detekt-baseline-main.xml b/kotlin-sdk-server/detekt-baseline-main.xml index a27f38a22..aad11fa6b 100644 --- a/kotlin-sdk-server/detekt-baseline-main.xml +++ b/kotlin-sdk-server/detekt-baseline-main.xml @@ -2,29 +2,6 @@ - InjectDispatcher:FeatureNotificationService.kt:FeatureNotificationService$Default - LongParameterList:KtorServer.kt:private suspend fun RoutingContext.streamableTransport: StreamableHttpServerTransport? - LongParameterList:StreamableHttpServerTransport.kt:StreamableHttpServerTransport.Configuration - MagicNumber:StdioServerTransport.kt:StdioServerTransport$8192 - MaxLineLength:SSEServerTransport.kt:SseServerTransport$"SSEServerTransport already started! If using Server class, note that connect() calls start() automatically." - MaxLineLength:SSEServerTransport.kt:SseServerTransport$* - NoNameShadowing:FeatureNotificationService.kt:FeatureNotificationService${ it.remove(job) } - ReturnCount:KtorServer.kt:private suspend fun existingStreamableTransport: StreamableHttpServerTransport? - TooGenericExceptionCaught:SSEServerTransport.kt:SseServerTransport$e: Exception - TooGenericExceptionCaught:StdioServerTransport.kt:StdioServerTransport$e: Throwable - TooManyFunctions:KtorServer.kt:io.modelcontextprotocol.kotlin.sdk.server.KtorServer.kt UnsafeCallOnNullableType:StreamableHttpServerTransport.kt:StreamableHttpServerTransport$responseRequestId!! - UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to add prompt '${prompt.name}': Server does not support prompts capability" } "Server does not support prompts capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to add prompts: Server does not support prompts capability" } "Server does not support prompts capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to remove prompt '$name': Server does not support prompts capability" } "Server does not support prompts capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to remove prompts: Server does not support prompts capability" } "Server does not support prompts capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to add resource '$name': Server does not support resources capability" } "Server does not support resources capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to add resources: Server does not support resources capability" } "Server does not support resources capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to remove resource '$uri': Server does not support resources capability" } "Server does not support resources capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to remove resources: Server does not support resources capability" } "Server does not support resources capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" } "Server does not support tools capability. Enable it in ServerOptions." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to add tools: Server does not support tools capability" } "Server does not support tools capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to remove tool '$name': Server does not support tools capability" } "Server does not support tools capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to remove tools: Server does not support tools capability" } "Server does not support tools capability." } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt index e64b0da81..9b4b2f35b 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt @@ -33,7 +33,6 @@ private val logger = KotlinLogging.logger {} * communication through notifications, requests, and other operations. * This interface defines various methods to facilitate the interaction. */ -@Suppress("TooManyFunctions") public interface ClientConnection { /** @@ -176,7 +175,6 @@ public interface ClientConnection { public suspend fun sendElicitationComplete(notification: ElicitationCompleteNotification) } -@Suppress("TooManyFunctions") internal class ClientConnectionImpl(private val session: ServerSession) : ClientConnection { override val sessionId: String get() = session.sessionId diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt index ce55f3e98..a892c6c6f 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt @@ -64,6 +64,7 @@ public data class RegisteredResource( * A registered resource template with its associated read handler. * * @property resourceTemplate The [ResourceTemplate] definition (RFC 6570 URI template). + * @property matcher Pre-built matcher used to test incoming URIs against [resourceTemplate]. * @property readHandler A suspend function invoked when a client reads a URI that matches * this template. The second parameter contains the URI variables extracted from the match. */ diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureNotificationService.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureNotificationService.kt index d996b2db4..42c2b8fb6 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureNotificationService.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureNotificationService.kt @@ -38,6 +38,7 @@ private sealed class NotificationEvent(open val timestamp: Long) /** * Represents an event for a notification. * + * @property timestamp Timestamp at which this event was created. * @property notification The notification associated with the event. */ private class SendEvent(override val timestamp: Long, val notification: Notification) : NotificationEvent(timestamp) @@ -340,7 +341,7 @@ internal class FeatureNotificationService( emitJobs.getAndUpdate { it.add(job) } // Register completion - job.invokeOnCompletion { + job.invokeOnCompletion { _ -> emitJobs.getAndUpdate { it.remove(job) } } 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 806a47916..3f03a7a09 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 @@ -1,5 +1,3 @@ -@file:Suppress("TooManyFunctions") - package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging @@ -172,7 +170,6 @@ private fun Application.mcpStreamableHttp( * that creates and returns the [Server] to handle the connection. */ @KtorDsl -@Suppress("LongParameterList") public fun Application.mcpStreamableHttp( path: String = "/mcp", enableDnsRebindingProtection: Boolean = false, @@ -247,7 +244,6 @@ private fun Application.mcpStatelessStreamableHttp( * that creates and returns the [Server] to handle the connection. */ @KtorDsl -@Suppress("LongParameterList") public fun Application.mcpStatelessStreamableHttp( path: String = "/mcp", enableDnsRebindingProtection: Boolean = false, diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt index fcf1a466c..a6d461833 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt @@ -27,13 +27,15 @@ internal const val SESSION_ID_PARAM = "sessionId" * * Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`. * - * @property sessionId unique identifier for this transport session, generated randomly on creation + * @param endpoint relative or absolute URL the client will POST messages to + * @param session active SSE session used to deliver server-to-client events */ @OptIn(ExperimentalAtomicApi::class) public class SseServerTransport(private val endpoint: String, private val session: ServerSSESession) : AbstractTransport() { private val initialized: AtomicBoolean = AtomicBoolean(false) + /** Unique identifier for this transport session, generated randomly on creation. */ @OptIn(ExperimentalUuidApi::class) public val sessionId: String = Uuid.random().toString() diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index d9b9b9a78..1b7bfe7f0 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -64,7 +64,8 @@ private val logger = KotlinLogging.logger {} * Configuration options for the MCP server. * * @property capabilities The capabilities this server supports. - * @property enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with clients. + * @param enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with clients. + * Forwarded to [ProtocolOptions.enforceStrictCapabilities]. * @property resourceTemplateMatcherFactory The factory used to create [ResourceTemplateMatcher] instances * for matching resource URIs against registered templates. Defaults to [PathSegmentTemplateMatcher.factory]. */ @@ -92,13 +93,12 @@ public class ServerOptions( * Currently, after subscription to a resource, the server will NOT send the subscription confirmation * as this response schema is not defined in the protocol. * - * @param serverInfo Information about this server implementation (name, version). - * @param options Configuration options for the server. - * @param instructionsProvider Optional provider for instructions from the server to the client about how to use + * @property serverInfo Information about this server implementation (name, version). + * @property options Configuration options for the server. + * @property instructionsProvider Optional provider for instructions from the server to the client about how to use * this server. The provider is called each time a new session is started to support dynamic instructions. * @param block A block to configure the mcp server. */ -@Suppress("TooManyFunctions") public open class Server( protected val serverInfo: Implementation, protected val options: ServerOptions, @@ -312,9 +312,9 @@ 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 title An optional human-readable name of the tool for display purposes. * @param outputSchema The optional expected output schema for the tool. * @param toolAnnotations Optional additional tool information. * @param execution Optional execution-related properties, such as task-augmented execution support. @@ -322,7 +322,6 @@ public open class Server( * @param handler A suspend function that handles executing the tool when called by the client. * @throws IllegalStateException If the server does not support tools. */ - @Suppress("LongParameterList") public fun addTool( name: String, description: String, @@ -645,7 +644,6 @@ public open class Server( } // Execute the tool handler and catch any errors - @Suppress("TooGenericExceptionCaught") return try { logger.trace { "Executing tool ${requestParams.name} with input: ${requestParams.arguments}" } tool.run { diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt index 2f55c7642..e7da26f62 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -39,17 +39,28 @@ import kotlin.uuid.Uuid private val logger = KotlinLogging.logger {} /** - * Represents a server session. + * Represents an active server-side session in the Model Context Protocol. * - * @property sessionId unique identifier for this session, generated randomly on creation + * Owns the server side of a single client connection on top of [Protocol]. Drives the + * initialize/initialized handshake, tracks the negotiated client capabilities and version, + * asserts capability requirements before dispatching requests and notifications, and exposes + * server-to-client operations such as sampling, root listing, elicitation, logging, and + * list-change notifications. + * + * Equality and hashing are based on [sessionId], so a session can be used as a key in + * routing structures. + * + * @property serverInfo server name and version reported to the client during initialization + * @param options server capabilities and protocol options used by [Protocol] + * @property instructions optional human-readable instructions for the client, sent in the initialize result */ -@Suppress("TooManyFunctions") public open class ServerSession( protected val serverInfo: Implementation, options: ServerOptions, protected val instructions: String?, ) : Protocol(options) { + /** Unique identifier for this session, generated on creation. */ @OptIn(ExperimentalUuidApi::class) public val sessionId: String = Uuid.random().toString() @@ -60,14 +71,10 @@ public open class ServerSession( private val _clientCapabilities: AtomicRef = atomic(null) private val _clientVersion: AtomicRef = atomic(null) - /** - * The client's reported capabilities after initialization. - */ + /** Capabilities reported by the client during initialization, or `null` before the handshake completes. */ public val clientCapabilities: ClientCapabilities? get() = _clientCapabilities.value - /** - * The client's version information after initialization. - */ + /** Client implementation information reported during initialization, or `null` before the handshake completes. */ public val clientVersion: Implementation? get() = _clientVersion.value /** diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt index 8fd46dd21..a4d588cc5 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt @@ -72,7 +72,6 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: private fun launchReadingJob(): Job { val job = scope.launch { val buf = Buffer() - @Suppress("TooGenericExceptionCaught") try { while (isActive) { val bytesRead = inputStream.readAtMostTo(buf, READ_BUFFER_SIZE) @@ -103,7 +102,6 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: private fun launchProcessingJob(): Job { val job = scope.launch { - @Suppress("TooGenericExceptionCaught") try { for (chunk in readChannel) { readBuffer.append(chunk) @@ -123,7 +121,6 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: private fun launchSendingJob(): Job { val job = scope.launch { - @Suppress("TooGenericExceptionCaught") try { for (message in writeChannel) { val json = serializeMessage(message) @@ -147,7 +144,6 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: } private suspend fun processReadBuffer() { - @Suppress("TooGenericExceptionCaught") while (true) { val message = try { readBuffer.readMessage() diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index f2320012d..870a92cf0 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -74,10 +74,8 @@ private data class SessionContext(val session: ServerSSESession?, val call: Appl * - No session validation is performed * * @param configuration Transport configuration. See [Configuration] for available options. - * @property sessionId session identifier assigned after initialization, or `null` in stateless mode */ @OptIn(ExperimentalUuidApi::class, ExperimentalAtomicApi::class) -@Suppress("TooManyFunctions") public class StreamableHttpServerTransport(private val configuration: Configuration) : AbstractTransport() { @Deprecated("Use default constructor with explicit Configuration()") @@ -100,7 +98,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat * @param retryIntervalMillis Retry interval in milliseconds for event handling or reconnection attempts. * Defaults to `null`. */ - @Suppress("MaxLineLength") @Deprecated( "Use constructor with Configuration: StreamableHttpServerTransport(Configuration(enableJsonResponse = ...))", replaceWith = ReplaceWith( @@ -153,6 +150,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } + /** Session identifier assigned after initialization in stateful mode, or `null` in stateless mode. */ public var sessionId: String? = null private set @@ -215,7 +213,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("CyclomaticComplexMethod", "ReturnCount") override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { val responseRequestId: RequestId? = when (message) { is JSONRPCResponse -> message.id @@ -329,7 +326,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat /** * Handles POST requests containing JSON-RPC messages */ - @Suppress("CyclomaticComplexMethod", "LongMethod", "ReturnCount", "TooGenericExceptionCaught") public suspend fun handlePostRequest(session: ServerSSESession?, call: ApplicationCall) { try { if (!configuration.enableJsonResponse && session == null) { @@ -438,7 +434,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } /** Handles an HTTP GET request by establishing a standalone SSE stream for server-initiated notifications. */ - @Suppress("ReturnCount") public suspend fun handleGetRequest(session: ServerSSESession?, call: ApplicationCall) { // NOTE: enableJsonResponse only controls how POST responses are delivered (JSON vs. SSE). // The standalone GET SSE stream is always supported — it is the only channel available @@ -512,7 +507,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat * Closes the SSE stream associated with the given [requestId], prompting the client to reconnect. * Useful for implementing polling behavior for long-running operations. */ - @Suppress("ReturnCount", "TooGenericExceptionCaught") public suspend fun closeSseStream(requestId: RequestId) { if (configuration.enableJsonResponse) return val streamId = requestToStreamMapping[requestId] ?: return @@ -529,7 +523,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("TooGenericExceptionCaught") private suspend fun replayEvents(store: EventStore, lastEventId: String, session: ServerSSESession) { val call: ApplicationCall = session.call @@ -596,7 +589,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("ReturnCount") private suspend fun validateSession(call: ApplicationCall): Boolean { if (sessionIdGenerator == null) return true @@ -655,7 +647,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("ReturnCount") private fun validateHeaders(call: ApplicationCall): String? { if (!configuration.enableDnsRebindingProtection) return null @@ -680,7 +671,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat return null } - @Suppress("TooGenericExceptionCaught") private suspend fun flushSse(session: ServerSSESession?) { try { session?.send(data = "") @@ -691,7 +681,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("ReturnCount") private suspend fun parseBody(call: ApplicationCall): List? { val maxSize = configuration.maxRequestBodySize val contentLength = call.request.header(HttpHeaders.ContentLength)?.toLongOrNull() ?: 0L @@ -752,7 +741,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("TooGenericExceptionCaught") private suspend fun maybeSendPrimingEvent( streamId: String, session: ServerSSESession?, diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt index c2cb2c455..a05daefa5 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt @@ -1,5 +1,3 @@ -@file:Suppress("TooManyFunctions") - package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractKtorExtensionsTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractKtorExtensionsTest.kt index 28e21b9c5..7639bc6a5 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractKtorExtensionsTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractKtorExtensionsTest.kt @@ -14,7 +14,6 @@ import io.ktor.utils.io.readUTF8Line import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities -@Suppress("AbstractClassCanBeConcreteClass") abstract class AbstractKtorExtensionsTest { protected val sseContentType = ContentType("text", "event-stream") diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt index dcbb7a4b8..32123a693 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt @@ -299,21 +299,18 @@ class StdioServerTransportTest { server.close() } - @Suppress("unused") private fun inputErrors() = listOf( IOException("simulated read failure"), RuntimeException("unexpected read exception"), OutOfMemoryError("unexpected read error"), ) - @Suppress("unused") private fun outputErrors() = listOf( IOException("simulated write failure"), RuntimeException("unexpected write exception"), OutOfMemoryError("unexpected write error"), ) - @Suppress("unused") private fun handlerErrors() = listOf( RuntimeException("handler failure"), IOException("handler IO failure"), diff --git a/kotlin-sdk-testing/build.gradle.kts b/kotlin-sdk-testing/build.gradle.kts index e318f6f09..bbb038812 100644 --- a/kotlin-sdk-testing/build.gradle.kts +++ b/kotlin-sdk-testing/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("mcp.multiplatform") + id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) diff --git a/kotlin-sdk-testing/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport.kt b/kotlin-sdk-testing/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport.kt index e6d19f7b3..ae5f487ce 100644 --- a/kotlin-sdk-testing/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport.kt +++ b/kotlin-sdk-testing/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport.kt @@ -27,9 +27,8 @@ import kotlinx.coroutines.yield * communication of JSON-RPC messages. This is useful in scenarios where communication is * message-based and channels are used as the underlying mechanism for delivery. * - * @constructor Initializes the transport with the specified [SendChannel] and [ReceiveChannel]. - * @property sendChannel The channel used to send JSON-RPC messages. - * @property receiveChannel The channel used to receive JSON-RPC messages. + * @param sendChannel The channel used to send JSON-RPC messages. + * @param receiveChannel The channel used to receive JSON-RPC messages. * @param dispatcher The coroutine dispatcher for the event loop. Defaults to [Dispatchers.Default]. */ @ExperimentalMcpApi @@ -75,11 +74,19 @@ public class ChannelTransport( /** * Creates a pair of interconnected [ChannelTransport] objects for bidirectional communication. * - * This method sets up a client and server transport using Kotlin channels, enabling messages sent - * through one transport to be received by the other. It is typically used for testing or in-memory - * communication without external networking. + * Sets up a client and server transport using Kotlin channels, so that messages sent through one + * transport are received by the other. Typically used for testing or in-memory communication + * without external networking. + * + * Example: + * ```kotlin + * val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + * server.connect(serverTransport) + * client.connect(clientTransport) + * ``` * * @param capacity The buffer capacity of the internal channels used for communication. Defaults to 256. + * @param dispatcher The coroutine dispatcher for the event loop. Defaults to [Dispatchers.Default]. * @return A [LinkedTransports] instance containing the client and server transports. */ public fun createLinkedPair( @@ -106,7 +113,6 @@ public class ChannelTransport( override suspend fun initialize() { logger.info { "ChannelTransport starting message processing" } val started = CompletableDeferred() - @Suppress("TooGenericExceptionCaught") scope.launch(CoroutineName("ChannelTransport#${hashCode()}-event-loop")) { try { // Signal that event loop has started, then yield to ensure we're ready diff --git a/kotlin-sdk-testing/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt b/kotlin-sdk-testing/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt index 7fc41bb0e..4aae932e9 100644 --- a/kotlin-sdk-testing/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt +++ b/kotlin-sdk-testing/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt @@ -186,7 +186,6 @@ class ChannelTransportTest { val id = ((msg as JSONRPCRequest).id as RequestId.NumberId).value.toInt() received.add(id) if (id == 2) { - @Suppress("TooGenericExceptionThrown") throw RuntimeException("Error processing message 2") } if (received.size == 4) { diff --git a/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt b/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt index 59faee4ff..13bf2724b 100644 --- a/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt +++ b/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt @@ -1,25 +1,3 @@ -@file:Suppress("ktlint:standard:no-empty-class-body", "ktlint:standard:kdoc") -/** - * # MCP Kotlin SDK - * - * A Kotlin Multiplatform implementation of the Model Context Protocol (MCP). - * - * This is the main SDK module that provides a convenient single dependency - * for all MCP functionality including: - * - * - Core protocol types and utilities ([kotlin-sdk-core]) - * - Client implementations ([kotlin-sdk-client]) - * - Server implementations ([kotlin-sdk-server]) - * - * ## Usage - * - * Add this dependency to your project to get access to all MCP Kotlin SDK functionality: - * - * ```kotlin - * implementation("io.modelcontextprotocol:kotlin-sdk:$version") - * ``` - * - * This will transitively include all core, client, and server components. - */ - package io.modelcontextprotocol.kotlin.sdk + +private class Stub diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts index 6e9125717..c6baf2d6f 100644 --- a/test-utils/build.gradle.kts +++ b/test-utils/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("mcp.multiplatform") + id("mcp.detekt") } kotlin { diff --git a/test-utils/detekt-baseline-main.xml b/test-utils/detekt-baseline-main.xml new file mode 100644 index 000000000..431f3d96a --- /dev/null +++ b/test-utils/detekt-baseline-main.xml @@ -0,0 +1,20 @@ + + + + + ForbiddenMethodCall:Retry.kt:RetryExtension$println( "[Retry] Failure on attempt $attempt/$maxAttempts for ${ describeTest( extensionContext, ) }: ${lastError.message}", ) + ForbiddenMethodCall:Retry.kt:RetryExtension$println( "[Retry] Giving up after $attempt attempts for ${ describeTest( extensionContext, ) }: ${lastError.message}", ) + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Adding exports to ${file.absolutePath}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Command failed: ${e.message}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Detected pnpm-specific protocols or workspace. Attempting pnpm install...") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Executing: ${fullCommand.joinToString(" ")}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Installing TypeScript dependencies in ${typescriptDir.absolutePath}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("[TS-INSTALL] $it") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("npm install failed. Patching package.json and trying again...") + ForbiddenMethodCall:processUtils.kt:println("Warning: Error reading process error stream: ${e.message}") + ForbiddenMethodCall:processUtils.kt:println("Warning: Error reading process output: ${e.message}") + ForbiddenMethodCall:processUtils.kt:println("[$prefix] $it") + ForbiddenMethodCall:processUtils.kt:println("[$prefix][err] $it") + ForbiddenVoid:Retry.kt:RetryExtension$Void + + diff --git a/test-utils/detekt-baseline.xml b/test-utils/detekt-baseline.xml deleted file mode 100644 index 5abd0b4e0..000000000 --- a/test-utils/detekt-baseline.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - CyclomaticComplexMethod:Retry.kt:RetryExtension$private fun executeWithRetry - ThrowsCount:Retry.kt:RetryExtension$private fun executeWithRetry - - diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt index a37e6e3b3..1dcf585e0 100644 --- a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt @@ -16,7 +16,6 @@ import java.util.Optional @ExtendWith(RetryExtension::class) public annotation class Retry(val times: Int = 3, val delayMs: Long = 1000) -@Suppress("ForbiddenVoid") // needed for Java API interop public class RetryExtension : InvocationInterceptor { override fun interceptTestMethod( invocation: Invocation, diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt index e7f5798f4..ff9292e3d 100644 --- a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt @@ -18,7 +18,6 @@ public object TypeScriptRunner { * @param logPrefix Prefix for log lines * @return The started Process */ - @Suppress("LongParameterList") public fun run( typescriptDir: File, scriptPath: String, @@ -54,7 +53,6 @@ public object TypeScriptRunner { * @param typescriptDir The directory containing package.json * @throws RuntimeException if npm install fails */ - @Suppress("ReturnCount") public fun installDependencies(typescriptDir: File) { require(typescriptDir.isDirectory()) { "Type script directory does not exist: ${typescriptDir.absolutePath}" } println("Installing TypeScript dependencies in ${typescriptDir.absolutePath}") diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt index 71fcfa668..a80c82548 100644 --- a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt @@ -14,7 +14,6 @@ import kotlin.time.Duration.Companion.seconds */ public fun killProcessOnPort(port: Int) { val killCommand = if (isWindows) { - @Suppress("MaxLineLength") listOf( "cmd.exe", "/c", From c339d8cb8656ae419d6149b3342a568ba0119351 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 29 Apr 2026 13:27:42 +0200 Subject: [PATCH 118/187] release: bump version to 0.12.0 (#731) 0.11.1 -> 0.12.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ca4a217ed..e145506f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,4 +8,4 @@ kotlin.daemon.jvmargs=-Xmx4G kotlin.native.ignoreDisabledTargets=true group=io.modelcontextprotocol -version=0.11.1 +version=0.12.0 From 5cb3c1de1219e36abdb409856a4d6edaf3e8b779 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 5 May 2026 11:05:59 +0200 Subject: [PATCH 119/187] fix: enable DNS rebinding protection by default (#659) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `DnsRebindingProtection` Ktor route-scoped plugin and enable it by default across all http transports ## Motivation and Context DNS rebinding protection was disabled by default (`enableDnsRebindingProtection = false`), and sse endpoints (`Route.mcp()`) had zero protection. Same vulnerability as typescript sdk [GHSA-w48q-cv73-mx4w](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-w48q-cv73-mx4w) ## How Has This Been Tested? all tests pass ## Breaking Changes - `Route.mcp()`, `Application.mcp()` signatures changed (new `enableDnsRebindingProtection`, `allowedHosts`, `allowedOrigins` params), binary incompatible on JVM - `enableDnsRebindingProtection` default changed from `false` to `true` on `mcpStreamableHttp`/`mcpStatelessStreamableHttp` - Default allowed hosts: `localhost`, `127.0.0.1`, `[::1]` — opt out with `enableDnsRebindingProtection = false` - Host validation is now port-agnostic (matches ts sdk) - Origin validation now allows missing `Origin` header (non-browser clients) - `Configuration.enableDnsRebindingProtection`, `.allowedHosts`, `.allowedOrigins` deprecated in favor of the plugin ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .../sdk/conformance/ConformanceServer.kt | 2 - kotlin-sdk-server/api/kotlin-sdk-server.api | 21 +- .../kotlin/sdk/server/HostValidation.kt | 185 +++++++++ .../kotlin/sdk/server/KtorServer.kt | 99 ++++- .../server/StreamableHttpServerTransport.kt | 31 +- .../sdk/server/DnsRebindingProtectionTest.kt | 382 ++++++++++++++++++ .../server/KtorApplicationExtensionsTest.kt | 4 +- .../sdk/server/KtorRouteExtensionsTest.kt | 6 +- .../StreamableHttpServerTransportTest.kt | 8 + 9 files changed, 703 insertions(+), 35 deletions(-) create mode 100644 kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt create mode 100644 kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/DnsRebindingProtectionTest.kt diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt index 03d5ee39e..8a1895750 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt @@ -12,8 +12,6 @@ fun main() { val port = System.getenv("MCP_PORT")?.toIntOrNull() ?: 3001 embeddedServer(CIO, port = port) { mcpStreamableHttp( - enableDnsRebindingProtection = true, - allowedHosts = listOf("localhost", "127.0.0.1", "localhost:$port", "127.0.0.1:$port"), eventStore = InMemoryEventStore(), ) { createConformanceServer() diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index 80da5b53e..f400e80d8 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -32,16 +32,31 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/ClientConnection$De public static synthetic fun ping$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } +public final class io/modelcontextprotocol/kotlin/sdk/server/DnsRebindingProtectionConfig { + public fun ()V + public final fun getAllowedHosts ()Ljava/util/List; + public final fun getAllowedOrigins ()Ljava/util/List; + public final fun setAllowedHosts (Ljava/util/List;)V + public final fun setAllowedOrigins (Ljava/util/List;)V +} + public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/EventStore { public abstract fun getStreamIdForEventId (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun replayEventsAfter (Ljava/lang/String;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun storeEvent (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class io/modelcontextprotocol/kotlin/sdk/server/HostValidationKt { + public static final fun getDnsRebindingProtection ()Lio/ktor/server/application/RouteScopedPlugin; +} + public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt { - public static final fun mcp (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function1;)V - public static final fun mcp (Lio/ktor/server/routing/Route;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public static final fun mcp (Lio/ktor/server/routing/Route;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/application/Application;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/routing/Route;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V + public static final fun mcp (Lio/ktor/server/routing/Route;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun mcp$default (Lio/ktor/server/application/Application;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun mcp$default (Lio/ktor/server/routing/Route;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static synthetic fun mcp$default (Lio/ktor/server/routing/Route;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public static final fun mcpStatelessStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;)V public static synthetic fun mcpStatelessStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public static final fun mcpStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;)V diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt new file mode 100644 index 000000000..88b56bc17 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt @@ -0,0 +1,185 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.parseUrl +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.RouteScopedPlugin +import io.ktor.server.application.createRouteScopedPlugin +import io.ktor.server.request.header +import io.ktor.server.response.respondText +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.RPCError + +/** + * Default list of hostnames allowed for localhost DNS rebinding protection. + * Matches the TypeScript SDK's `localhostAllowedHostnames()`. + */ +internal val LOCALHOST_ALLOWED_HOSTS: List = listOf("localhost", "127.0.0.1", "[::1]") + +/** + * Characters that are valid in a URL but must not appear in an HTTP `Host` header. + * Rejecting them prevents the parser from accepting malformed values + * (e.g. `evil.com@localhost`, `host/path`) that a generic URL parser would silently allow. + */ +private val FORBIDDEN_HOST_CHARS: CharArray = charArrayOf('/', '@', '?', '#') + +/** + * Extracts the hostname from a Host header value, stripping the port. + * + * Only accepts the strict `host [ ":" port ]` / `"[" ipv6 "]" [ ":" port ]` + * format defined by RFC 7230. Values containing URL-only characters + * (`/`, `@`, `?`, `#`) or whitespace are rejected. + * + * Examples: + * - `"localhost:3000"` → `"localhost"` + * - `"127.0.0.1:8080"` → `"127.0.0.1"` + * - `"[::1]:3000"` → `"[::1]"` + * - `"example.com"` → `"example.com"` + * - `"evil.com@localhost"` → `null` + * + * @return the hostname, or `null` if the value is blank, malformed, or contains forbidden characters. + */ +internal fun extractHostname(hostHeader: String): String? = when { + hostHeader.isBlank() -> null + + hostHeader.any { it in FORBIDDEN_HOST_CHARS || it.isWhitespace() } -> null + + hostHeader.startsWith("[") -> { + val end = hostHeader.indexOf(']') + when { + end <= 1 -> null + isValidPortSuffix(hostHeader.substring(end + 1)) -> hostHeader.substring(0, end + 1) + else -> null + } + } + + else -> { + val colon = hostHeader.indexOf(':') + when { + colon < 0 -> hostHeader + colon == 0 -> null + isValidPortSuffix(hostHeader.substring(colon)) -> hostHeader.substring(0, colon) + else -> null + } + } +} + +/** + * Checks that [tail] is either empty (no port part) or `":"` followed by zero or more digits, + * matching RFC 7230 §3.2.3 where `port = *DIGIT`. Non-empty non-digit characters are rejected. + */ +private fun isValidPortSuffix(tail: String): Boolean = when { + tail.isEmpty() -> true + tail[0] != ':' -> false + else -> tail.substring(1).all { it.isDigit() } +} + +/** + * Extracts the hostname from an `Origin` header value. + * + * Uses Ktor's [parseUrl] to avoid reimplementing RFC 6454 parsing. + * Scheme, port, userinfo, and path are discarded — only the host matters for + * DNS-rebinding protection. The result is **not** normalized to lower-case; + * callers should apply [String.lowercase] if they need case-insensitive comparison. + * + * @return the hostname, or `null` if [origin] cannot be parsed or has no host part. + */ +internal fun extractOriginHost(origin: String): String? = parseUrl(origin)?.host?.takeIf(String::isNotEmpty) + +/** + * Configuration for the [DnsRebindingProtection] Ktor route-scoped plugin. + * + * @property allowedHosts List of hostnames allowed in the `Host` header. + * Comparison is port-agnostic and case-insensitive. + * Defaults to `localhost`, `127.0.0.1`, `[::1]`. + * An empty list will reject **all** requests. + * @property allowedOrigins Optional list of allowed `Origin` values. Entries are parsed as URLs + * (via [parseUrl]) and compared by **hostname only** — scheme and port are ignored. + * If `null`, origin validation is disabled. + * If configured, requests **with** an `Origin` header whose hostname is not in the list + * are rejected, but requests **without** an `Origin` header are allowed (non-browser clients). + */ +public class DnsRebindingProtectionConfig { + public var allowedHosts: List = LOCALHOST_ALLOWED_HOSTS + public var allowedOrigins: List? = null +} + +/** + * Ktor route-scoped plugin that validates `Host` and `Origin` headers + * to protect against DNS rebinding attacks. + * + * Install on a route to intercept all requests **before** handlers: + * ```kotlin + * route("/mcp") { + * install(DnsRebindingProtection) { + * allowedHosts = listOf("myapp.com", "localhost") + * } + * // handlers... + * } + * ``` + */ +public val DnsRebindingProtection: RouteScopedPlugin = + createRouteScopedPlugin( + "MCP-DnsRebindingProtection", + ::DnsRebindingProtectionConfig, + ) { + val hosts: Set = pluginConfig.allowedHosts.mapTo(mutableSetOf()) { + extractHostname(it)?.lowercase() + ?: error("Invalid host in DnsRebindingProtection.allowedHosts: '$it'") + } + val originHosts: Set? = pluginConfig.allowedOrigins?.mapTo(mutableSetOf()) { + extractOriginHost(it)?.lowercase() + ?: error("Invalid origin in DnsRebindingProtection.allowedOrigins: '$it'") + } + + onCall { call -> + val hostHeader = call.request.header(HttpHeaders.Host) + val hostname = hostHeader?.let { extractHostname(it) }?.lowercase() + + if (hostname == null) { + call.rejectDnsValidation("Invalid Host header: (malformed or missing)") + return@onCall + } + if (hostname !in hosts) { + call.rejectDnsValidation("Invalid Host: $hostname") + return@onCall + } + + if (originHosts != null) { + val originHeader = call.request.header(HttpHeaders.Origin) + // Allow requests without Origin (non-browser clients cannot perform DNS rebinding) + if (originHeader != null) { + val originHost = extractOriginHost(originHeader)?.lowercase() + if (originHost == null) { + call.rejectDnsValidation("Invalid Origin header: (unparseable)") + return@onCall + } + if (originHost !in originHosts) { + call.rejectDnsValidation("Invalid Origin host: $originHost") + return@onCall + } + } + } + } + } + +/** + * Responds with a 403 Forbidden JSON-RPC error without requiring ContentNegotiation. + */ +private suspend fun ApplicationCall.rejectDnsValidation(message: String) { + val error = JSONRPCError( + id = null, + error = RPCError( + code = -32_000, + message = message, + ), + ) + respondText( + McpJson.encodeToString(error), + ContentType.Application.Json, + HttpStatusCode.Forbidden, + ) +} 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 3f03a7a09..d6191dffb 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 @@ -37,14 +37,26 @@ private val logger = KotlinLogging.logger {} * Use [Application.mcp] if you want SSE to be installed automatically. * * @param path the URL path to register the SSE endpoint. + * @param enableDnsRebindingProtection whether to install [DnsRebindingProtection] on this route. Defaults to `true`. + * @param allowedHosts hostnames allowed in the `Host` header. Defaults to `localhost`, `127.0.0.1`, `[::1]`. + * @param allowedOrigins origins allowed in the `Origin` header, compared by hostname only + * (scheme and port are ignored). Requests without an `Origin` header are allowed. + * Pass `null` to skip origin validation. * @param block factory block with access to the [ServerSSESession] * that creates and returns the [Server] to handle the connection. * @throws IllegalStateException if the [SSE] plugin is not installed. */ @KtorDsl -public fun Route.mcp(path: String, block: ServerSSESession.() -> Server) { +@Suppress("LongParameterList") +public fun Route.mcp( + path: String, + enableDnsRebindingProtection: Boolean = true, + allowedHosts: List? = null, + allowedOrigins: List? = null, + block: ServerSSESession.() -> Server, +) { route(path) { - mcp(block) + mcp(enableDnsRebindingProtection, allowedHosts, allowedOrigins, block) } } @@ -55,12 +67,22 @@ public fun Route.mcp(path: String, block: ServerSSESession.() -> Server) { * **Precondition:** the [SSE] plugin must be installed on the application before calling this function. * Use [Application.mcp] if you want SSE to be installed automatically. * + * @param enableDnsRebindingProtection whether to install [DnsRebindingProtection] on this route. Defaults to `true`. + * @param allowedHosts hostnames allowed in the `Host` header. Defaults to `localhost`, `127.0.0.1`, `[::1]`. + * @param allowedOrigins origins allowed in the `Origin` header, compared by hostname only + * (scheme and port are ignored). Requests without an `Origin` header are allowed. + * Pass `null` to skip origin validation. * @param block factory block with access to the [ServerSSESession] * that creates and returns the [Server] to handle the connection. * @throws IllegalStateException if the [SSE] plugin is not installed. */ @KtorDsl -public fun Route.mcp(block: ServerSSESession.() -> Server) { +public fun Route.mcp( + enableDnsRebindingProtection: Boolean = true, + allowedHosts: List? = null, + allowedOrigins: List? = null, + block: ServerSSESession.() -> Server, +) { try { plugin(SSE) } catch (e: MissingApplicationPluginException) { @@ -72,6 +94,8 @@ public fun Route.mcp(block: ServerSSESession.() -> Server) { ) } + installDnsRebindingProtection(enableDnsRebindingProtection, allowedHosts, allowedOrigins) + val transportManager = TransportManager() sse { @@ -91,21 +115,35 @@ public fun Route.mcp(block: ServerSSESession.() -> Server) { * Automatically installs [ContentNegotiation][io.ktor.server.plugins.contentnegotiation.ContentNegotiation] * with [McpJson][io.modelcontextprotocol.kotlin.sdk.types.McpJson] and [SSE]. * + * @param enableDnsRebindingProtection whether to install [DnsRebindingProtection] on this route. Defaults to `true`. + * @param allowedHosts hostnames allowed in the `Host` header. Defaults to `localhost`, `127.0.0.1`, `[::1]`. + * @param allowedOrigins origins allowed in the `Origin` header, compared by hostname only + * (scheme and port are ignored). Requests without an `Origin` header are allowed. + * Pass `null` to skip origin validation. * @param block factory block with access to the [ServerSSESession] * that creates and returns the [Server] to handle the connection. */ @KtorDsl -public fun Application.mcp(block: ServerSSESession.() -> Server) { +public fun Application.mcp( + enableDnsRebindingProtection: Boolean = true, + allowedHosts: List? = null, + allowedOrigins: List? = null, + block: ServerSSESession.() -> Server, +) { installMcpContentNegotiation() install(SSE) routing { - mcp(block) + mcp(enableDnsRebindingProtection, allowedHosts, allowedOrigins, block) } } +@Suppress("LongParameterList") private fun Application.mcpStreamableHttp( path: String = "/mcp", + enableDnsRebindingProtection: Boolean, + allowedHosts: List?, + allowedOrigins: List?, configuration: StreamableHttpServerTransport.Configuration, block: RoutingContext.() -> Server, ) { @@ -116,6 +154,8 @@ private fun Application.mcpStreamableHttp( routing { route(path) { + installDnsRebindingProtection(enableDnsRebindingProtection, allowedHosts, allowedOrigins) + // Set Mcp-Session-Id on GET responses before Ktor's sse {} commits headers. intercept(ApplicationCallPipeline.Plugins) { if (context.request.httpMethod == HttpMethod.Get) { @@ -160,10 +200,12 @@ private fun Application.mcpStreamableHttp( * with [McpJson][io.modelcontextprotocol.kotlin.sdk.types.McpJson] and [SSE]. * * @param path The base path for the MCP Streamable HTTP endpoint. Defaults to "/mcp". - * @param enableDnsRebindingProtection Enables DNS rebinding attack protection for the endpoint. Defaults to false. - * @param allowedHosts A list of hostnames allowed to access the endpoint. If `null`, no restrictions are applied. - * @param allowedOrigins A list of origins allowed to perform cross-origin requests (CORS). - * If `null`, no restrictions are applied. + * @param enableDnsRebindingProtection Enables DNS rebinding attack protection for the endpoint. Defaults to `true`. + * @param allowedHosts A list of hostnames allowed to access the endpoint. + * If `null` and DNS rebinding protection is enabled, defaults to `localhost`, `127.0.0.1`, `[::1]`. + * @param allowedOrigins A list of allowed `Origin` header values, compared by hostname only + * (scheme and port are ignored). Requests without an `Origin` header are allowed. + * If `null`, origin validation is disabled. * @param eventStore An optional [EventStore] instance to enable resumable event stream functionality. * Allows storing and replaying events. * @param block factory block with access to the [RoutingContext] (for reading request headers) @@ -172,7 +214,7 @@ private fun Application.mcpStreamableHttp( @KtorDsl public fun Application.mcpStreamableHttp( path: String = "/mcp", - enableDnsRebindingProtection: Boolean = false, + enableDnsRebindingProtection: Boolean = true, allowedHosts: List? = null, allowedOrigins: List? = null, eventStore: EventStore? = null, @@ -180,10 +222,10 @@ public fun Application.mcpStreamableHttp( ) { mcpStreamableHttp( path = path, + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, configuration = StreamableHttpServerTransport.Configuration( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, eventStore = eventStore, enableJsonResponse = true, ), @@ -191,8 +233,12 @@ public fun Application.mcpStreamableHttp( ) } +@Suppress("LongParameterList") private fun Application.mcpStatelessStreamableHttp( path: String = "/mcp", + enableDnsRebindingProtection: Boolean, + allowedHosts: List?, + allowedOrigins: List?, configuration: StreamableHttpServerTransport.Configuration, block: RoutingContext.() -> Server, ) { @@ -201,6 +247,8 @@ private fun Application.mcpStatelessStreamableHttp( routing { route(path) { + installDnsRebindingProtection(enableDnsRebindingProtection, allowedHosts, allowedOrigins) + post { mcpStatelessStreamableHttpEndpoint( configuration = configuration, @@ -236,9 +284,12 @@ private fun Application.mcpStatelessStreamableHttp( * with [McpJson][io.modelcontextprotocol.kotlin.sdk.types.McpJson] and [SSE]. * * @param path The URL path where the server listens for incoming JSON-RPC requests. Defaults to "/mcp". - * @param enableDnsRebindingProtection Determines whether DNS rebinding protection is enabled. Defaults to `false`. - * @param allowedHosts A list of allowed hostnames. If null, host filtering is disabled. - * @param allowedOrigins A list of allowed origins for CORS. If null, origin filtering is disabled. + * @param enableDnsRebindingProtection Determines whether DNS rebinding protection is enabled. Defaults to `true`. + * @param allowedHosts A list of allowed hostnames. If `null` and DNS rebinding protection is enabled, + * defaults to `localhost`, `127.0.0.1`, `[::1]`. + * @param allowedOrigins A list of allowed `Origin` header values, compared by hostname only + * (scheme and port are ignored). Requests without an `Origin` header are allowed. + * If `null`, origin validation is disabled. * @param eventStore An optional [EventStore] implementation to provide resumability and event replay support. * @param block factory block with access to the [RoutingContext] (for reading request headers) * that creates and returns the [Server] to handle the connection. @@ -246,7 +297,7 @@ private fun Application.mcpStatelessStreamableHttp( @KtorDsl public fun Application.mcpStatelessStreamableHttp( path: String = "/mcp", - enableDnsRebindingProtection: Boolean = false, + enableDnsRebindingProtection: Boolean = true, allowedHosts: List? = null, allowedOrigins: List? = null, eventStore: EventStore? = null, @@ -254,10 +305,10 @@ public fun Application.mcpStatelessStreamableHttp( ) { mcpStatelessStreamableHttp( path = path, + enableDnsRebindingProtection = enableDnsRebindingProtection, + allowedHosts = allowedHosts, + allowedOrigins = allowedOrigins, configuration = StreamableHttpServerTransport.Configuration( - enableDnsRebindingProtection = enableDnsRebindingProtection, - allowedHosts = allowedHosts, - allowedOrigins = allowedOrigins, eventStore = eventStore, enableJsonResponse = true, ), @@ -395,3 +446,11 @@ private suspend fun RoutingContext.streamableTransport( return transport } + +private fun Route.installDnsRebindingProtection(enabled: Boolean, hosts: List?, origins: List?) { + if (!enabled) return + install(DnsRebindingProtection) { + allowedHosts = hosts ?: LOCALHOST_ALLOWED_HOSTS + origins?.let { allowedOrigins = it } + } +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 870a92cf0..ae8a39d1d 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -136,8 +136,20 @@ public class StreamableHttpServerTransport(private val configuration: Configurat */ public class Configuration( public val enableJsonResponse: Boolean = false, + @Deprecated( + message = "Use install(DnsRebindingProtection) on your Ktor route instead", + level = DeprecationLevel.WARNING, + ) public val enableDnsRebindingProtection: Boolean = false, + @Deprecated( + message = "Use install(DnsRebindingProtection) on your Ktor route instead", + level = DeprecationLevel.WARNING, + ) public val allowedHosts: List? = null, + @Deprecated( + message = "Use install(DnsRebindingProtection) on your Ktor route instead", + level = DeprecationLevel.WARNING, + ) public val allowedOrigins: List? = null, public val eventStore: EventStore? = null, public val retryInterval: Duration? = null, @@ -300,7 +312,12 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } /** - * Handles an incoming HTTP request, whether GET, POST or DELETE + * Handles an incoming HTTP request, whether GET, POST or DELETE. + * + * **Note:** this method does not perform DNS rebinding protection on its own. + * When using this transport outside the [mcpStreamableHttp] / [mcpStatelessStreamableHttp] + * DSL, install the [DnsRebindingProtection] plugin on your Ktor route + * (or apply equivalent middleware) to validate `Host` / `Origin` headers. */ public suspend fun handleRequest(session: ServerSSESession?, call: ApplicationCall) { validateHeaders(call)?.let { reason -> @@ -651,10 +668,13 @@ public class StreamableHttpServerTransport(private val configuration: Configurat if (!configuration.enableDnsRebindingProtection) return null configuration.allowedHosts?.let { hosts -> - val hostHeader = call.request.headers[HttpHeaders.Host]?.lowercase() - val allowedHostsLowercase = hosts.map { it.lowercase() } + val hostHeader = call.request.headers[HttpHeaders.Host] + val hostname = hostHeader?.let { extractHostname(it) }?.lowercase() + val allowedHostsLowercase = hosts.map { + extractHostname(it)?.lowercase() ?: it.lowercase() + } - if (hostHeader == null || hostHeader !in allowedHostsLowercase) { + if (hostname == null || hostname !in allowedHostsLowercase) { return "Invalid Host header: $hostHeader" } } @@ -663,7 +683,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat val originHeader = call.request.headers[HttpHeaders.Origin]?.lowercase() val allowedOriginsLowercase = origins.map { it.lowercase() } - if (originHeader == null || originHeader !in allowedOriginsLowercase) { + // Allow requests without Origin (non-browser clients cannot perform DNS rebinding) + if (originHeader != null && originHeader !in allowedOriginsLowercase) { return "Invalid Origin header: $originHeader" } } diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/DnsRebindingProtectionTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/DnsRebindingProtectionTest.kt new file mode 100644 index 000000000..96c44efbe --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/DnsRebindingProtectionTest.kt @@ -0,0 +1,382 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.ktor.client.shouldHaveStatus +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.server.application.install +import io.ktor.server.response.respondText +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.server.testing.ApplicationTestBuilder +import io.ktor.server.testing.testApplication +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.provider.ValueSource +import kotlin.test.Test + +class DnsRebindingProtectionTest { + + companion object { + @JvmStatic + fun invalidHostCases(): List = listOf( + Arguments.of("", "Invalid Host header: (malformed or missing)"), + Arguments.of("evil.com", "Invalid Host: evil.com"), + ) + + @JvmStatic + fun extractHostnameAcceptCases(): List = listOf( + Arguments.of("localhost", "localhost"), + Arguments.of("localhost:3000", "localhost"), + Arguments.of("127.0.0.1:8080", "127.0.0.1"), + Arguments.of("[::1]", "[::1]"), + Arguments.of("[::1]:3000", "[::1]"), + Arguments.of("localhost:", "localhost"), + Arguments.of("[::1]:", "[::1]"), + ) + + @JvmStatic + fun extractHostnameRejectCases(): List = listOf( + "", // empty + "evil.com@localhost", // userinfo + "evil.com/path", // path + "evil.com?q=1", // query + "evil.com#frag", // fragment + "[::1", // malformed IPv6 + "[]", // empty brackets + "[]:3000", // empty brackets + port + " localhost", // leading whitespace + "localhost ", // trailing whitespace + "localhost\t:80", // embedded tab + "localhost:abc", // non-numeric port + "localhost:-1", // negative port + "[::1]:abc", // non-numeric IPv6 port + ":80", // leading colon + ) + + @JvmStatic + fun extractOriginHostAcceptCases(): List = listOf( + Arguments.of("http://example.com", "example.com"), + Arguments.of("http://example.com:8080", "example.com"), + Arguments.of("https://Example.COM", "Example.COM"), + Arguments.of("https://example.com", "example.com"), + ) + + @JvmStatic + fun extractOriginHostRejectCases(): List = listOf( + "example.com", // no scheme + "not-a-url", // unparseable + "", // empty + ) + } + + private fun testWithPlugin( + config: DnsRebindingProtectionConfig.() -> Unit = {}, + test: suspend ApplicationTestBuilder.() -> Unit, + ): Unit = testApplication { + application { + install(SSE) + routing { + route("/mcp") { + install(DnsRebindingProtection, config) + post { call.respondText("ok") } + } + } + } + test() + } + + @ParameterizedTest + @ValueSource(strings = ["localhost", "127.0.0.1", "[::1]", "localhost:3000", "[::1]:8080"]) + fun `plugin accepts valid Host header`(hostHeader: String) = testWithPlugin { + val response = client.post("/mcp") { + header(HttpHeaders.Host, hostHeader) + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @ParameterizedTest + @MethodSource("invalidHostCases") + fun `plugin rejects invalid Host header`(hostHeader: String, expectedBody: String) = testWithPlugin { + val response = client.post("/mcp") { + header(HttpHeaders.Host, hostHeader) + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + response.bodyAsText() shouldContain expectedBody + } + + @Test + fun `plugin does not echo raw malformed Host in rejection`() = testWithPlugin { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "evil.com@localhost") + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + response.bodyAsText() shouldNotContain "evil.com@localhost" + response.bodyAsText() shouldContain "malformed or missing" + } + + @Test + fun `plugin allows request without Origin header`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin rejects disallowed Origin`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "http://evil.com") + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + response.bodyAsText() shouldContain "Invalid Origin host: evil.com" + } + + @Test + fun `plugin allows matching Origin`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "http://localhost:3000") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin allows Origin with different port but same hostname`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "http://localhost:9999") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin allows Origin with different scheme but same hostname`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "https://localhost:3000") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin rejects unparseable Origin header`() = testWithPlugin( + config = { allowedOrigins = listOf("http://localhost:3000") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "not-a-url") + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + response.bodyAsText() shouldContain "Invalid Origin header: (unparseable)" + } + + @Test + fun `plugin with custom allowedHosts accepts matching host`() = testWithPlugin( + config = { allowedHosts = listOf("myapp.com") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "myapp.com:443") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin with custom allowedHosts rejects non-matching host`() = testWithPlugin( + config = { allowedHosts = listOf("myapp.com") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + } + + @Test + fun `host validation is case insensitive`() = testWithPlugin( + config = { allowedHosts = listOf("MyApp.COM") }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "myapp.com") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `origin validation is case insensitive`() = testWithPlugin( + config = { + allowedHosts = listOf("localhost") + allowedOrigins = listOf("https://MyApp.COM") + }, + ) { + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Origin, "https://myapp.com") + } + response.shouldHaveStatus(HttpStatusCode.OK) + response.bodyAsText() shouldBe "ok" + } + + @Test + fun `plugin with empty allowedHosts rejects all requests`() = testWithPlugin( + config = { allowedHosts = emptyList() }, + ) { + val localhostResponse = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + } + localhostResponse.shouldHaveStatus(HttpStatusCode.Forbidden) + localhostResponse.bodyAsText() shouldContain "Invalid Host: localhost" + + val otherResponse = client.post("/mcp") { + header(HttpHeaders.Host, "myapp.com") + } + otherResponse.shouldHaveStatus(HttpStatusCode.Forbidden) + otherResponse.bodyAsText() shouldContain "Invalid Host: myapp.com" + } + + @Test + fun `plugin fails to install when allowedHosts contains an invalid host`() { + val ex = shouldThrow { + testWithPlugin( + config = { allowedHosts = listOf("https://example.com") }, + ) { + client.post("/mcp") { header(HttpHeaders.Host, "example.com") } + } + } + ex.message shouldContain "https://example.com" + } + + @Test + fun `plugin fails to install when allowedOrigins contains an invalid origin`() { + val ex = shouldThrow { + testWithPlugin( + config = { allowedOrigins = listOf("not-a-url") }, + ) { + client.post("/mcp") { header(HttpHeaders.Host, "localhost") } + } + } + ex.message shouldContain "not-a-url" + } + + @Test + fun `Route mcp with DNS protection enabled rejects non-localhost`() = testApplication { + application { + install(SSE) + routing { + mcp { testServer() } + } + } + + val response = client.post("/") { + header(HttpHeaders.Host, "evil.com") + contentType(ContentType.Application.Json) + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + } + + @Test + fun `Route mcp with DNS protection disabled allows any host`() = testApplication { + application { + install(SSE) + routing { + mcp(enableDnsRebindingProtection = false) { testServer() } + } + } + + val response = client.post("/") { + header(HttpHeaders.Host, "evil.com") + contentType(ContentType.Application.Json) + } + // Not 403 — the request reaches the handler (may get 400 for missing sessionId, etc.) + response.shouldHaveStatus(HttpStatusCode.BadRequest) + } + + @Test + fun `mcpStreamableHttp rejects non-localhost Host by default`() = testApplication { + application { + mcpStreamableHttp { testServer() } + } + + val response = client.post("/mcp") { + header(HttpHeaders.Host, "evil.com") + contentType(ContentType.Application.Json) + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + } + + @Test + fun `mcpStatelessStreamableHttp rejects non-localhost Host by default`() = testApplication { + application { + mcpStatelessStreamableHttp { testServer() } + } + + val response = client.post("/mcp") { + header(HttpHeaders.Host, "evil.com") + contentType(ContentType.Application.Json) + } + response.shouldHaveStatus(HttpStatusCode.Forbidden) + } + + // -- extractHostname unit tests -- + + @ParameterizedTest + @MethodSource("extractHostnameAcceptCases") + fun `extractHostname accepts valid host`(input: String, expected: String) { + extractHostname(input) shouldBe expected + } + + @ParameterizedTest + @MethodSource("extractHostnameRejectCases") + fun `extractHostname rejects invalid host`(input: String) { + extractHostname(input) shouldBe null + } + + // -- extractOriginHost unit tests -- + + @ParameterizedTest + @MethodSource("extractOriginHostAcceptCases") + fun `extractOriginHost extracts hostname`(input: String, expected: String) { + extractOriginHost(input) shouldBe expected + } + + @ParameterizedTest + @MethodSource("extractOriginHostRejectCases") + fun `extractOriginHost returns null for invalid origin`(input: String) { + extractOriginHost(input) shouldBe null + } + + private fun testServer(): Server = Server( + serverInfo = Implementation(name = "test-server", version = "1.0.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) +} diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorApplicationExtensionsTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorApplicationExtensionsTest.kt index 05200e59b..988886791 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorApplicationExtensionsTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorApplicationExtensionsTest.kt @@ -24,9 +24,9 @@ class KtorApplicationExtensionsTest : AbstractKtorExtensionsTest() { * added to the same application. */ @Test - fun `Application mcp should installs SSE and coexist with other routes`() = testApplication { + fun `Application mcp should install SSE and coexist with other routes`() = testApplication { application { - mcp { testServer() } + mcp(enableDnsRebindingProtection = false) { testServer() } routing { get("/health") { call.respondText("healthy") } diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorRouteExtensionsTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorRouteExtensionsTest.kt index 5ba4448f6..d6b95b4fe 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorRouteExtensionsTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorRouteExtensionsTest.kt @@ -61,7 +61,7 @@ class KtorRouteExtensionsTest : AbstractKtorExtensionsTest() { route("/api/mcp") { get("/test") { call.respondText("test-endpoint") } - mcp { testServer() } + mcp(enableDnsRebindingProtection = false) { testServer() } } } } @@ -91,7 +91,7 @@ class KtorRouteExtensionsTest : AbstractKtorExtensionsTest() { route("/v1") { route("/services") { route("/mcp") { - mcp { testServer() } + mcp(enableDnsRebindingProtection = false) { testServer() } } } } @@ -112,7 +112,7 @@ class KtorRouteExtensionsTest : AbstractKtorExtensionsTest() { routing { route("/api") { - mcp("/mcp-endpoint") { testServer() } + mcp("/mcp-endpoint", enableDnsRebindingProtection = false) { testServer() } } } } diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt index 8b413d8a5..f6dda1520 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt @@ -477,6 +477,7 @@ class StreamableHttpServerTransportTest { // Step 1: Initialize session via POST val initResponse = client.post(mcpPath) { + header(HttpHeaders.Host, "localhost") addStreamableHeaders() setBody(buildInitializeRequestPayload()) } @@ -485,6 +486,7 @@ class StreamableHttpServerTransportTest { // Step 2: Open first GET SSE stream client.prepareGet(mcpPath) { + header(HttpHeaders.Host, "localhost") header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) header(MCP_SESSION_ID_HEADER, sessionId) header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) @@ -495,6 +497,7 @@ class StreamableHttpServerTransportTest { // Step 3: Open a second GET — the transport closes the old session // and the new stream takes over. client.prepareGet(mcpPath) { + header(HttpHeaders.Host, "localhost") header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) header(MCP_SESSION_ID_HEADER, sessionId) header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) @@ -528,6 +531,7 @@ class StreamableHttpServerTransportTest { // Step 1: Initialize session via POST val initResponse = client.post(mcpPath) { + header(HttpHeaders.Host, "localhost") addStreamableHeaders() setBody(buildInitializeRequestPayload()) } @@ -536,6 +540,7 @@ class StreamableHttpServerTransportTest { // Step 2: Open and then close a GET SSE stream client.prepareGet(mcpPath) { + header(HttpHeaders.Host, "localhost") header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) header(MCP_SESSION_ID_HEADER, sessionId) header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) @@ -547,6 +552,7 @@ class StreamableHttpServerTransportTest { // Step 3: Immediately reconnect — the transport should close the stale // stream and allow the new one. client.prepareGet(mcpPath) { + header(HttpHeaders.Host, "localhost") header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) header(MCP_SESSION_ID_HEADER, sessionId) header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) @@ -578,6 +584,7 @@ class StreamableHttpServerTransportTest { // Step 1: Initialize session via POST val initResponse = client.post(mcpPath) { + header(HttpHeaders.Host, "localhost") addStreamableHeaders() setBody(buildInitializeRequestPayload()) } @@ -586,6 +593,7 @@ class StreamableHttpServerTransportTest { // Step 2: Open GET SSE stream with session ID client.prepareGet(mcpPath) { + header(HttpHeaders.Host, "localhost") header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) header(MCP_SESSION_ID_HEADER, sessionId) header("mcp-protocol-version", LATEST_PROTOCOL_VERSION) From 49e1072c66a34d4af9100264abb728881bd41aac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 11:34:35 +0200 Subject: [PATCH 120/187] chore(deps): bump the infrastructure group across 1 directory with 2 updates (#752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the infrastructure group with 2 updates in the / directory: [io.github.oshai:kotlin-logging](https://github.com/oshai/kotlin-logging) and [io.netty:netty-bom](https://github.com/netty/netty). Updates `io.github.oshai:kotlin-logging` from 8.0.01 to 8.0.02
Release notes

Sourced from io.github.oshai:kotlin-logging's releases.

8.0.02

What's Changed

Full Changelog: https://github.com/oshai/kotlin-logging/compare/8.0.01...8.0.02

Commits
  • accd3ca bump version to 8.0.03
  • 2bd058a bump version to 8.0.02
  • 8ab1a8a Fix GraalVM native build error caused by stale substitution target in v8.x (#...
  • See full diff in compare view

Updates `io.netty:netty-bom` from 4.2.12.Final to 4.2.13.Final
Release notes

Sourced from io.netty:netty-bom's releases.

netty-4.2.13.Final

CVEs Fixed

  • CVE-2026-42586 (netty-codec-redis)
  • CVE-2026-42578 (netty-handler-proxy)
  • CVE-2026-42577 (netty-transport-native-epoll)
  • CVE-2026-42587 (netty-codec-http, netty-codec-http2)
  • CVE-2026-41417 (netty-codec-http)
  • CVE-2026-42581 (netty-codec-http)
  • CVE-2026-42580 (netty-codec-http)
  • CVE-2026-42585 (netty-codec-http)
  • CVE-2026-42579 (netty-codec-dns)
  • CVE-2026-42582 (netty-codec-http3)
  • CVE-2026-42583 (netty-codec, netty-codec-compression)
  • CVE-2026-42584 (netty-codec-http)
  • CVE-2026-XXXXX (netty-codec-mqtt)

What's Changed

... (truncated)

Commits
  • b3844c8 [maven-release-plugin] prepare release netty-4.2.13.Final
  • 82f47fa Merge commit from fork
  • ada0999 Merge commit from fork
  • b4051e2 Fix BrotliDecoder not forwarding all decompressed chunks
  • 67207c1 Merge commit from fork
  • 541ca7c Merge commit from fork
  • 943edb3 Fix codec-dns tests
  • 6459a28 Merge commit from fork
  • b4ba61b Fix checkstyle in HttpObjectDecoder
  • 977661f Merge commit from fork
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 360a468bf..63a75c574 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ kover = "0.9.8" ktlint = "14.2.0" knit = "0.5.1" mavenPublish = "0.36.0" -netty = "4.2.12.Final" +netty = "4.2.13.Final" # libraries version awaitility = "4.3.0" @@ -18,7 +18,7 @@ coroutines = "1.10.2" kotest = "6.1.11" kotlinx-io = "0.9.0" ktor = "3.3.3" -logging = "8.0.01" +logging = "8.0.02" mockk = "1.14.9" mokksy = "0.8.1" serialization = "1.11.0" From c1210009a5d3b9d5c89d21684f7d5cfd3ebbb17e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 11:53:25 +0200 Subject: [PATCH 121/187] chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.11.1 to 0.12.0 in /samples/kotlin-mcp-client (#743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [io.modelcontextprotocol:kotlin-sdk-client](https://github.com/modelcontextprotocol/kotlin-sdk) from 0.11.1 to 0.12.0.
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.12.0

Description

Adds sampling-with-tools per SEP-1577, JSON Schema dialect declaration on tool schemas, server-side tool name validation, and capability extensions; fixes SSE reconnect on the same session and stops swallowing CancellationException.

Breaking Changes

Sampling messages can carry tool calls and results (SEP-1577) by @​devcrocod in #718

SamplingMessage.content is now typed as a new SamplingMessageContent supertype, with MediaContent (text/image/audio), ToolUseContent, and ToolResultContent as variants. MediaContent still exists and now extends SamplingMessageContent, so most source code that constructs sampling messages compiles unchanged — but the binary signatures of SamplingMessage, CreateMessageResult, and related types have changed. ClientCapabilities.sampling is also now a typed Sampling object instead of a raw JsonObject. Recompilation is required; consumers reading sampling as JsonObject must migrate to the typed accessors.

watchosX64 and tvosX64 Kotlin/Native targets removed by @​devcrocod in #727

These targets have been deprecated upstream. Migrate to the corresponding ARM/simulator targets (watchosArm64, watchosSimulatorArm64, tvosArm64, tvosSimulatorArm64).

Features

  • Add $schema field to ToolSchema, declaring JSON Schema 2020-12 as the default dialect (SEP-1613) by @​devcrocod in #696
  • Validate tool names at registration time on the server, following the MCP tool naming standard (SEP-986) by @​devcrocod in #695
  • Add extensions field to ClientCapabilities and ServerCapabilities for advertising supported MCP extensions during the initialize handshake by @​eritscher in #678

Fixed

  • Validate the mcp-protocol-version HTTP header on initialization requests and correct DEFAULT_NEGOTIATED_PROTOCOL_VERSION to match the spec by @​devcrocod in #697
  • Propagate CancellationException instead of logging it as an error by @​devcrocod in #706
  • Evict stale GET SSE stream mapping on reconnect so a new GET on the same session is no longer silently rejected by the leftover entry from the previous stream by @​amr in #716

Maintenance

  • Add kotlinlang MCP server sample with search and get_page tools by @​devcrocod in #713
  • Migrate detekt to a buildSrc convention plugin with a fully explicit, version-locked YAML config by @​devcrocod in #729
  • Add Streamable HTTP integration tests for pagination, bad-request handling, and logging by @​jskjw157 in #613
  • Remove unused OpenAPI Generator plugin and configuration by @​devcrocod in #683
  • Remove Codecov configuration in favor of kover by @​devcrocod in #691
  • Remove redundant JUnit XML report configuration by @​devcrocod in #728
  • Remove IDEA files tied to the incompatible detekt IDE plugin by @​devcrocod in #714
  • Add Node.js setup to the release workflow so TypeScript integration tests run on macOS runners by @​devcrocod in #684

Dependencies

  • Kotlin to v2.3.21 in #704, #723
  • kotlinx.serialization-json to v1.11.0 in #687

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.1...0.12.0

Commits
  • c339d8c release: bump version to 0.12.0 (#731)
  • 64804a0 refactor: explicit locked detekt config (#729)
  • a238fc0 fix(server): evict stale GET SSE stream mapping on reconnect (#716)
  • 66d9efb test(streamable-http): add missing integration tests for pagination, bad requ...
  • f434fca feat!: SEP-1577 sampling with tools (#718)
  • 7474a33 chore: drop deprecated watchosX64 and tvosX64 targets (#727)
  • 12ace76 chore: remove redundant JUnit XML report configuration (#728)
  • ec606db chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-serv...
  • 1d93042 chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-server ...
  • 9817b17 chore(deps): bump org.jetbrains.kotlin.jvm from 2.2.21 to 2.3.21 in /samples/...
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 69ebddef5..dd1ec3f5a 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -2,7 +2,7 @@ anthropic = "2.27.0" kotlin = "2.3.21" ktor = "3.3.3" -mcp-kotlin = "0.11.1" +mcp-kotlin = "0.12.0" shadow = "9.4.1" slf4j = "2.0.17" From 9561624ba999dae728d38aaf4f4076e21e218557 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 10:13:00 +0000 Subject: [PATCH 122/187] chore(deps): bump mcp-kotlin from 0.11.1 to 0.12.0 in /samples/kotlin-mcp-server (#744) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.11.1 to 0.12.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.11.1 to 0.12.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.12.0

Description

Adds sampling-with-tools per SEP-1577, JSON Schema dialect declaration on tool schemas, server-side tool name validation, and capability extensions; fixes SSE reconnect on the same session and stops swallowing CancellationException.

Breaking Changes

Sampling messages can carry tool calls and results (SEP-1577) by @​devcrocod in #718

SamplingMessage.content is now typed as a new SamplingMessageContent supertype, with MediaContent (text/image/audio), ToolUseContent, and ToolResultContent as variants. MediaContent still exists and now extends SamplingMessageContent, so most source code that constructs sampling messages compiles unchanged — but the binary signatures of SamplingMessage, CreateMessageResult, and related types have changed. ClientCapabilities.sampling is also now a typed Sampling object instead of a raw JsonObject. Recompilation is required; consumers reading sampling as JsonObject must migrate to the typed accessors.

watchosX64 and tvosX64 Kotlin/Native targets removed by @​devcrocod in #727

These targets have been deprecated upstream. Migrate to the corresponding ARM/simulator targets (watchosArm64, watchosSimulatorArm64, tvosArm64, tvosSimulatorArm64).

Features

  • Add $schema field to ToolSchema, declaring JSON Schema 2020-12 as the default dialect (SEP-1613) by @​devcrocod in #696
  • Validate tool names at registration time on the server, following the MCP tool naming standard (SEP-986) by @​devcrocod in #695
  • Add extensions field to ClientCapabilities and ServerCapabilities for advertising supported MCP extensions during the initialize handshake by @​eritscher in #678

Fixed

  • Validate the mcp-protocol-version HTTP header on initialization requests and correct DEFAULT_NEGOTIATED_PROTOCOL_VERSION to match the spec by @​devcrocod in #697
  • Propagate CancellationException instead of logging it as an error by @​devcrocod in #706
  • Evict stale GET SSE stream mapping on reconnect so a new GET on the same session is no longer silently rejected by the leftover entry from the previous stream by @​amr in #716

Maintenance

  • Add kotlinlang MCP server sample with search and get_page tools by @​devcrocod in #713
  • Migrate detekt to a buildSrc convention plugin with a fully explicit, version-locked YAML config by @​devcrocod in #729
  • Add Streamable HTTP integration tests for pagination, bad-request handling, and logging by @​jskjw157 in #613
  • Remove unused OpenAPI Generator plugin and configuration by @​devcrocod in #683
  • Remove Codecov configuration in favor of kover by @​devcrocod in #691
  • Remove redundant JUnit XML report configuration by @​devcrocod in #728
  • Remove IDEA files tied to the incompatible detekt IDE plugin by @​devcrocod in #714
  • Add Node.js setup to the release workflow so TypeScript integration tests run on macOS runners by @​devcrocod in #684

Dependencies

  • Kotlin to v2.3.21 in #704, #723
  • kotlinx.serialization-json to v1.11.0 in #687

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.1...0.12.0

Commits
  • c339d8c release: bump version to 0.12.0 (#731)
  • 64804a0 refactor: explicit locked detekt config (#729)
  • a238fc0 fix(server): evict stale GET SSE stream mapping on reconnect (#716)
  • 66d9efb test(streamable-http): add missing integration tests for pagination, bad requ...
  • f434fca feat!: SEP-1577 sampling with tools (#718)
  • 7474a33 chore: drop deprecated watchosX64 and tvosX64 targets (#727)
  • 12ace76 chore: remove redundant JUnit XML report configuration (#728)
  • ec606db chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-serv...
  • 1d93042 chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-server ...
  • 9817b17 chore(deps): bump org.jetbrains.kotlin.jvm from 2.2.21 to 2.3.21 in /samples/...
  • Additional commits viewable in compare view

Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.11.1 to 0.12.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

0.12.0

Description

Adds sampling-with-tools per SEP-1577, JSON Schema dialect declaration on tool schemas, server-side tool name validation, and capability extensions; fixes SSE reconnect on the same session and stops swallowing CancellationException.

Breaking Changes

Sampling messages can carry tool calls and results (SEP-1577) by @​devcrocod in #718

SamplingMessage.content is now typed as a new SamplingMessageContent supertype, with MediaContent (text/image/audio), ToolUseContent, and ToolResultContent as variants. MediaContent still exists and now extends SamplingMessageContent, so most source code that constructs sampling messages compiles unchanged — but the binary signatures of SamplingMessage, CreateMessageResult, and related types have changed. ClientCapabilities.sampling is also now a typed Sampling object instead of a raw JsonObject. Recompilation is required; consumers reading sampling as JsonObject must migrate to the typed accessors.

watchosX64 and tvosX64 Kotlin/Native targets removed by @​devcrocod in #727

These targets have been deprecated upstream. Migrate to the corresponding ARM/simulator targets (watchosArm64, watchosSimulatorArm64, tvosArm64, tvosSimulatorArm64).

Features

  • Add $schema field to ToolSchema, declaring JSON Schema 2020-12 as the default dialect (SEP-1613) by @​devcrocod in #696
  • Validate tool names at registration time on the server, following the MCP tool naming standard (SEP-986) by @​devcrocod in #695
  • Add extensions field to ClientCapabilities and ServerCapabilities for advertising supported MCP extensions during the initialize handshake by @​eritscher in #678

Fixed

  • Validate the mcp-protocol-version HTTP header on initialization requests and correct DEFAULT_NEGOTIATED_PROTOCOL_VERSION to match the spec by @​devcrocod in #697
  • Propagate CancellationException instead of logging it as an error by @​devcrocod in #706
  • Evict stale GET SSE stream mapping on reconnect so a new GET on the same session is no longer silently rejected by the leftover entry from the previous stream by @​amr in #716

Maintenance

  • Add kotlinlang MCP server sample with search and get_page tools by @​devcrocod in #713
  • Migrate detekt to a buildSrc convention plugin with a fully explicit, version-locked YAML config by @​devcrocod in #729
  • Add Streamable HTTP integration tests for pagination, bad-request handling, and logging by @​jskjw157 in #613
  • Remove unused OpenAPI Generator plugin and configuration by @​devcrocod in #683
  • Remove Codecov configuration in favor of kover by @​devcrocod in #691
  • Remove redundant JUnit XML report configuration by @​devcrocod in #728
  • Remove IDEA files tied to the incompatible detekt IDE plugin by @​devcrocod in #714
  • Add Node.js setup to the release workflow so TypeScript integration tests run on macOS runners by @​devcrocod in #684

Dependencies

  • Kotlin to v2.3.21 in #704, #723
  • kotlinx.serialization-json to v1.11.0 in #687

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.1...0.12.0

Commits
  • c339d8c release: bump version to 0.12.0 (#731)
  • 64804a0 refactor: explicit locked detekt config (#729)
  • a238fc0 fix(server): evict stale GET SSE stream mapping on reconnect (#716)
  • 66d9efb test(streamable-http): add missing integration tests for pagination, bad requ...
  • f434fca feat!: SEP-1577 sampling with tools (#718)
  • 7474a33 chore: drop deprecated watchosX64 and tvosX64 targets (#727)
  • 12ace76 chore: remove redundant JUnit XML report configuration (#728)
  • ec606db chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-serv...
  • 1d93042 chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-server ...
  • 9817b17 chore(deps): bump org.jetbrains.kotlin.jvm from 2.2.21 to 2.3.21 in /samples/...
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index a6c07dc00..2eccae2eb 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.3.21" ktor = "3.3.3" -mcp-kotlin = "0.11.1" +mcp-kotlin = "0.12.0" slf4j = "2.0.17" shadow = "9.4.1" From 22e48089be89524dd6301fca30eec31775668711 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 10:27:26 +0000 Subject: [PATCH 123/187] chore(deps): bump mcp-kotlin from 0.11.1 to 0.12.0 in /samples/weather-stdio-server (#750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.11.1 to 0.12.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.11.1 to 0.12.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.12.0

Description

Adds sampling-with-tools per SEP-1577, JSON Schema dialect declaration on tool schemas, server-side tool name validation, and capability extensions; fixes SSE reconnect on the same session and stops swallowing CancellationException.

Breaking Changes

Sampling messages can carry tool calls and results (SEP-1577) by @​devcrocod in #718

SamplingMessage.content is now typed as a new SamplingMessageContent supertype, with MediaContent (text/image/audio), ToolUseContent, and ToolResultContent as variants. MediaContent still exists and now extends SamplingMessageContent, so most source code that constructs sampling messages compiles unchanged — but the binary signatures of SamplingMessage, CreateMessageResult, and related types have changed. ClientCapabilities.sampling is also now a typed Sampling object instead of a raw JsonObject. Recompilation is required; consumers reading sampling as JsonObject must migrate to the typed accessors.

watchosX64 and tvosX64 Kotlin/Native targets removed by @​devcrocod in #727

These targets have been deprecated upstream. Migrate to the corresponding ARM/simulator targets (watchosArm64, watchosSimulatorArm64, tvosArm64, tvosSimulatorArm64).

Features

  • Add $schema field to ToolSchema, declaring JSON Schema 2020-12 as the default dialect (SEP-1613) by @​devcrocod in #696
  • Validate tool names at registration time on the server, following the MCP tool naming standard (SEP-986) by @​devcrocod in #695
  • Add extensions field to ClientCapabilities and ServerCapabilities for advertising supported MCP extensions during the initialize handshake by @​eritscher in #678

Fixed

  • Validate the mcp-protocol-version HTTP header on initialization requests and correct DEFAULT_NEGOTIATED_PROTOCOL_VERSION to match the spec by @​devcrocod in #697
  • Propagate CancellationException instead of logging it as an error by @​devcrocod in #706
  • Evict stale GET SSE stream mapping on reconnect so a new GET on the same session is no longer silently rejected by the leftover entry from the previous stream by @​amr in #716

Maintenance

  • Add kotlinlang MCP server sample with search and get_page tools by @​devcrocod in #713
  • Migrate detekt to a buildSrc convention plugin with a fully explicit, version-locked YAML config by @​devcrocod in #729
  • Add Streamable HTTP integration tests for pagination, bad-request handling, and logging by @​jskjw157 in #613
  • Remove unused OpenAPI Generator plugin and configuration by @​devcrocod in #683
  • Remove Codecov configuration in favor of kover by @​devcrocod in #691
  • Remove redundant JUnit XML report configuration by @​devcrocod in #728
  • Remove IDEA files tied to the incompatible detekt IDE plugin by @​devcrocod in #714
  • Add Node.js setup to the release workflow so TypeScript integration tests run on macOS runners by @​devcrocod in #684

Dependencies

  • Kotlin to v2.3.21 in #704, #723
  • kotlinx.serialization-json to v1.11.0 in #687

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.1...0.12.0

Commits
  • c339d8c release: bump version to 0.12.0 (#731)
  • 64804a0 refactor: explicit locked detekt config (#729)
  • a238fc0 fix(server): evict stale GET SSE stream mapping on reconnect (#716)
  • 66d9efb test(streamable-http): add missing integration tests for pagination, bad requ...
  • f434fca feat!: SEP-1577 sampling with tools (#718)
  • 7474a33 chore: drop deprecated watchosX64 and tvosX64 targets (#727)
  • 12ace76 chore: remove redundant JUnit XML report configuration (#728)
  • ec606db chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-serv...
  • 1d93042 chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-server ...
  • 9817b17 chore(deps): bump org.jetbrains.kotlin.jvm from 2.2.21 to 2.3.21 in /samples/...
  • Additional commits viewable in compare view

Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.11.1 to 0.12.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

0.12.0

Description

Adds sampling-with-tools per SEP-1577, JSON Schema dialect declaration on tool schemas, server-side tool name validation, and capability extensions; fixes SSE reconnect on the same session and stops swallowing CancellationException.

Breaking Changes

Sampling messages can carry tool calls and results (SEP-1577) by @​devcrocod in #718

SamplingMessage.content is now typed as a new SamplingMessageContent supertype, with MediaContent (text/image/audio), ToolUseContent, and ToolResultContent as variants. MediaContent still exists and now extends SamplingMessageContent, so most source code that constructs sampling messages compiles unchanged — but the binary signatures of SamplingMessage, CreateMessageResult, and related types have changed. ClientCapabilities.sampling is also now a typed Sampling object instead of a raw JsonObject. Recompilation is required; consumers reading sampling as JsonObject must migrate to the typed accessors.

watchosX64 and tvosX64 Kotlin/Native targets removed by @​devcrocod in #727

These targets have been deprecated upstream. Migrate to the corresponding ARM/simulator targets (watchosArm64, watchosSimulatorArm64, tvosArm64, tvosSimulatorArm64).

Features

  • Add $schema field to ToolSchema, declaring JSON Schema 2020-12 as the default dialect (SEP-1613) by @​devcrocod in #696
  • Validate tool names at registration time on the server, following the MCP tool naming standard (SEP-986) by @​devcrocod in #695
  • Add extensions field to ClientCapabilities and ServerCapabilities for advertising supported MCP extensions during the initialize handshake by @​eritscher in #678

Fixed

  • Validate the mcp-protocol-version HTTP header on initialization requests and correct DEFAULT_NEGOTIATED_PROTOCOL_VERSION to match the spec by @​devcrocod in #697
  • Propagate CancellationException instead of logging it as an error by @​devcrocod in #706
  • Evict stale GET SSE stream mapping on reconnect so a new GET on the same session is no longer silently rejected by the leftover entry from the previous stream by @​amr in #716

Maintenance

  • Add kotlinlang MCP server sample with search and get_page tools by @​devcrocod in #713
  • Migrate detekt to a buildSrc convention plugin with a fully explicit, version-locked YAML config by @​devcrocod in #729
  • Add Streamable HTTP integration tests for pagination, bad-request handling, and logging by @​jskjw157 in #613
  • Remove unused OpenAPI Generator plugin and configuration by @​devcrocod in #683
  • Remove Codecov configuration in favor of kover by @​devcrocod in #691
  • Remove redundant JUnit XML report configuration by @​devcrocod in #728
  • Remove IDEA files tied to the incompatible detekt IDE plugin by @​devcrocod in #714
  • Add Node.js setup to the release workflow so TypeScript integration tests run on macOS runners by @​devcrocod in #684

Dependencies

  • Kotlin to v2.3.21 in #704, #723
  • kotlinx.serialization-json to v1.11.0 in #687

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.1...0.12.0

Commits
  • c339d8c release: bump version to 0.12.0 (#731)
  • 64804a0 refactor: explicit locked detekt config (#729)
  • a238fc0 fix(server): evict stale GET SSE stream mapping on reconnect (#716)
  • 66d9efb test(streamable-http): add missing integration tests for pagination, bad requ...
  • f434fca feat!: SEP-1577 sampling with tools (#718)
  • 7474a33 chore: drop deprecated watchosX64 and tvosX64 targets (#727)
  • 12ace76 chore: remove redundant JUnit XML report configuration (#728)
  • ec606db chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-serv...
  • 1d93042 chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-server ...
  • 9817b17 chore(deps): bump org.jetbrains.kotlin.jvm from 2.2.21 to 2.3.21 in /samples/...
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index 19874f44f..b294c8010 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ coroutines = "1.10.2" kotlin = "2.3.21" ktor = "3.3.3" logging = "8.0.01" -mcp-kotlin = "0.11.1" +mcp-kotlin = "0.12.0" shadow = "9.4.1" slf4j = "2.0.17" From 3e4a5e0c1d4f508c660cff0094a30b2033eb0810 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 14:18:24 +0200 Subject: [PATCH 124/187] chore(deps): bump kotlin from 2.3.20 to 2.3.21 in /samples/kotlinlang-mcp-server (#748) Bumps `kotlin` from 2.3.20 to 2.3.21. Updates `org.jetbrains.kotlin.jvm` from 2.3.20 to 2.3.21
Release notes

Sourced from org.jetbrains.kotlin.jvm's releases.

Kotlin 2.3.21

Changelog

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin.jvm's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'
Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlin.plugin.serialization` from 2.3.20 to 2.3.21
Release notes

Sourced from org.jetbrains.kotlin.plugin.serialization's releases.

Kotlin 2.3.21

Changelog

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin.plugin.serialization's changelog.

2.3.21

Backend. Wasm

  • KT-84610 [Wasm] Failed to compile klibs in IC mode

Compiler

  • KT-84566 Prevent launching Default dispatcher threads from IJ SDK in kotlin compiler
  • KT-85358 Native: roll back the workaround for KT-84678 once MapLibre has been properly fixed
  • KT-85626 @JvmRecord in commonMain breaks compileCommonMainKotlinMetadata with "Cannot access 'java.lang.Record'"
  • KT-85405 Postpone/Revert DontIgnoreUpperBoundViolatedOnImplicitArguments
  • KT-84678 K/N: Undefined symbol from SPM-added ObjC frameworks when linking iOS target
  • KT-85021 False positive SUBCLASS_CANT_CALL_COMPANION_PROTECTED_NON_STATIC error in multi-module project

JavaScript

  • KT-82395 Support top-level declarations from compiler plugins in JS incremental compilation
  • KT-84475 K/JS: false-positive exportability warnings in multi-module project
  • KT-84633 Kotlin/JS: "Serializer for class not found" error when IR output granularity is whole-program
  • KT-85047 Kotlin/JS: @JsStatic on suspend fun of class companion generates incorrect d.ts
  • KT-84517 K/JS: bad mappings data in outputted Kotlin stdlib source map

Libraries

  • KT-71848 Kotlinx.metadata: Add CompilerPluginData into Km API

Native. C and ObjC Import

  • KT-85399 Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass with genericSafeCasts enabled
  • KT-85508 K/N: TypeCastException when using nw_parameters_create_secure_tcp block parameter on 2.3.20

Tools. Gradle

  • KT-84729 Update Gradle plugin-publish version to enable configuration cache badge on Gradle plugins portal

Tools. Gradle. Compiler plugins

  • KT-85257 AGP/Compose: MergeMappingFileTask clears R8 artifacts due to @OutputDirectory annotation on AGP 9.1+

Tools. Scripts

  • KT-85105 Scripts: JVM backend internal error (IR lowering) when scratch file contains anonymous object
  • KT-85103 Exception while generating code when explain destructuring decls
  • KT-84842 scriptCompilationClasspathFromContext behavior changed from 2.3.10 to 2.3.20
  • KT-85029 Kotlin Scripting: ScriptDiagnostic reports "at null" instead of error location

Tools. Statistics (FUS)

  • KT-85628 KGP: composite build FUS metrics fail on access of 'configurationTimeMetrics'
Commits
  • fea1ad8 Add ChangeLog for 2.3.21-RC2
  • 09c341e disable swift export execution tests in order to update macos
  • 67a0868 Avoid accessing KotlinNativeLink taskProvider when task was not executed
  • f89e5db [K/N] Disable TSAN in runtime tests
  • 45d6c85 [K/N] Don't generate generic safe casts for Objective-C types
  • 9261a6f [K/N][tests] Add a reproducer for KT-85508
  • c9ab9db [K/N][tests] Add a reproducer for KT-85399
  • 502e844 Explain: fix for destructuring declarations
  • 0c26485 Explain: fix for object literals
  • 68a9e3f [minor] fix testdata name in explain test
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlinlang-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml index 20dff78b5..967e7ff9f 100644 --- a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.3.20" +kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.11.1" serialization = "1.10.0" From b0093523566e8c4facd3dbde46a7a7cb1997c325 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 15:04:13 +0200 Subject: [PATCH 125/187] chore(deps): bump org.jetbrains.kotlinx:kotlinx-serialization-json from 1.10.0 to 1.11.0 in /samples/kotlinlang-mcp-server (#746) Bumps [org.jetbrains.kotlinx:kotlinx-serialization-json](https://github.com/Kotlin/kotlinx.serialization) from 1.10.0 to 1.11.0.
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-serialization-json's releases.

1.11.0

This release is based on Kotlin 2.3.20 and provides a new Json exceptions API and some bugfixes and improvements.

Expose Json exceptions structure

To make working with exceptions easier and providing proper error codes in e.g., REST APIs, classes JsonException, JsonDecodingException, and JsonEncodingException are now public. They have relevant public properties, such as shortMessage, path, offset, and others. This API is currently experimental, and we're going to improve it further in the subsequent releases. See the linked issues for the details: #1930, #1877.

Ability to hide user input from exception messages for security/privacy reasons.

Historically, exception messages in kotlinx.serialization often included the input Json itself for debuggability reason. Such behavior may pose additional challenges for logging, analytics, and other systems, since a system is not always allowed to store user data due to privacy/security reasons, which imposes additional sanitation logic. To address this issue, a new property exceptionsWithDebugInfo is added to JsonConfiguration. Disable it to hide user input from exception messages. IMPORTANT: This behavior will be enabled by default when this property becomes stable. See #2590 for more details.

Bugfixes and improvements

  • CBOR: Relax value range check when decoding numbers (#3167)
  • Use a specialized writeDecimalLong method for IO stream integrations in Json (#3152)
Changelog

Sourced from org.jetbrains.kotlinx:kotlinx-serialization-json's changelog.

1.11.0 / 2026-04-10

This release is based on Kotlin 2.3.20 and provides new Json exceptions API and some bugfixes and improvements.

Expose Json exceptions structure

To make working with exceptions easier and providing proper error codes in e.g., REST APIs, classes JsonException, JsonDecodingException, and JsonEncodingException are now public. They have relevant public properties, such as shortMessage, path, offset, and others. This API is currently experimental, and we're going to improve it further in the subsequent releases. See the linked issues for the details: #1930, #1877.

Ability to hide user input from exception messages for security/privacy reasons.

Historically, exception messages in kotlinx.serialization often included the input Json itself for debuggability reason. Such behavior may pose additional challenges for logging, analytics, and other systems, since a system is not always allowed to store user data due to privacy/security reasons, which imposes additional sanitation logic. To address this issue, a new property exceptionsWithDebugInfo is added to JsonConfiguration. Disable it to hide user input from exception messages. IMPORTANT: This behavior will be enabled by default when this property becomes stable. See #2590 for more details.

Bugfixes and improvements

  • CBOR: Relax value range check when decoding numbers (#3167)
  • Use a specialized writeDecimalLong method for IO stream integrations in Json (#3152)
Commits
  • 6956af2 Prepare 1.11 release
  • 390d84c Merge remote-tracking branch 'origin/master' into dev
  • 431fe2d Use local repo for publishing (#3171)
  • 05c12b6 Add usage attribute to "testRepositories" configuration
  • a4e1f08 Bump Kover version to 0.9.8 release (#3174)
  • 304e858 Expose Json exceptions structure (#3145)
  • 4a0338e Included G Play SDK verification file for core-jvm (#3169)
  • 421f64c CBOR: Relax value range check when decoding numbers (#3167)
  • 85a4f12 KT-84955: mark apple x64 tagets as deprecated error
  • bd38b0e Remove dead code
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlinlang-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml index 967e7ff9f..2150c1070 100644 --- a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.11.1" -serialization = "1.10.0" +serialization = "1.11.0" coroutines = "1.10.2" logback = "1.5.32" shadow = "9.4.1" From 93e5ed2c06a1e88b9981b92713557fc5f16acb4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 16:03:42 +0200 Subject: [PATCH 126/187] chore(deps): bump the other-dependencies group across 5 directories with 3 updates (#753) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the other-dependencies group with 2 updates in the /samples/kotlin-mcp-client directory: [gradle-wrapper](https://github.com/gradle/gradle) and [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java). Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-server directory: [gradle-wrapper](https://github.com/gradle/gradle). Bumps the other-dependencies group with 1 update in the /samples/kotlinlang-mcp-server directory: [gradle-wrapper](https://github.com/gradle/gradle). Bumps the other-dependencies group with 1 update in the /samples/simple-streamable-server directory: [gradle-wrapper](https://github.com/gradle/gradle). Bumps the other-dependencies group with 2 updates in the /samples/weather-stdio-server directory: [gradle-wrapper](https://github.com/gradle/gradle) and [io.github.oshai:kotlin-logging-jvm](https://github.com/oshai/kotlin-logging). Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Updates `com.anthropic:anthropic-java` from 2.27.0 to 2.28.0
Release notes

Sourced from com.anthropic:anthropic-java's releases.

v2.28.0

2.28.0 (2026-05-04)

Full Changelog: v2.27.0...v2.28.0

Features

  • api: improve Managed Agents APIs (8453f3e)
  • client: add Workload Identity Federation, interactive OAuth, and auth profiles (b5f1d02)
  • client: more robust error parsing (d523547)
  • support setting headers via env (03bb766)

Chores

Documentation

  • clarify forwards compat behavior (f48cfb4)
Changelog

Sourced from com.anthropic:anthropic-java's changelog.

2.28.0 (2026-05-04)

Full Changelog: v2.27.0...v2.28.0

Features

  • api: improve Managed Agents APIs (8453f3e)
  • client: add Workload Identity Federation, interactive OAuth, and auth profiles (b5f1d02)
  • client: more robust error parsing (d523547)
  • support setting headers via env (03bb766)

Chores

Documentation

  • clarify forwards compat behavior (f48cfb4)
Commits
  • e448de8 chore: release main
  • b1899db feat(client): more robust error parsing
  • 959647a codegen metadata
  • c8cc791 feat(client): add Workload Identity Federation, interactive OAuth, and auth p...
  • 14e9142 docs: clarify forwards compat behavior
  • ac01a78 codegen metadata
  • 5dda387 codegen metadata
  • 84b5f41 codegen metadata
  • af86b69 codegen metadata
  • 8194aad ci: pin trigger-release-please github action
  • Additional commits viewable in compare view

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Updates `gradle-wrapper` from 9.4.1 to 9.5.0
Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Van... _Description has been truncated_ Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 48966 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/kotlin-mcp-client/gradlew | 2 +- samples/kotlin-mcp-client/gradlew.bat | 31 +--- .../gradle/wrapper/gradle-wrapper.jar | Bin 48966 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/kotlin-mcp-server/gradlew | 2 +- samples/kotlin-mcp-server/gradlew.bat | 31 +--- .../gradle/wrapper/gradle-wrapper.jar | Bin 48966 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/kotlinlang-mcp-server/gradlew | 2 +- samples/kotlinlang-mcp-server/gradlew.bat | 175 ++++++++---------- .../gradle/wrapper/gradle-wrapper.jar | Bin 48966 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/simple-streamable-server/gradlew | 2 +- samples/simple-streamable-server/gradlew.bat | 31 +--- .../gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 48966 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/weather-stdio-server/gradlew | 2 +- samples/weather-stdio-server/gradlew.bat | 31 +--- 22 files changed, 144 insertions(+), 189 deletions(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index dd1ec3f5a..2959a9a1a 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -anthropic = "2.27.0" +anthropic = "2.28.0" kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.12.0" diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar index d997cfc60f4cff0e7451d19d49a82fa986695d07..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 39760 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 e(*aw}>J`&Ep$5$o2mVP)i30-M3~u@DBh0 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 delta 40115 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 e{Sg5G5zENPNVU~b5#<2@M#Os+0czL&{q{eZe&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 +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ 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 +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -: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 +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar index d997cfc60f4cff0e7451d19d49a82fa986695d07..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 39760 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 e(*aw}>J`&Ep$5$o2mVP)i30-M3~u@DBh0 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 delta 40115 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 e{Sg5G5zENPNVU~b5#<2@M#Os+0czL&{q{eZe&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 +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ 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 +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -: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 +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.jar b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.jar index d997cfc60f4cff0e7451d19d49a82fa986695d07..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 39760 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 e(*aw}>J`&Ep$5$o2mVP)i30-M3~u@DBh0 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 delta 40115 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 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 - - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -: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 +@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, and ensure extensions are enabled +setlocal EnableExtensions + +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 + +"%COMSPEC%" /c exit 1 + +: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 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.jar b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.jar index d997cfc60f4cff0e7451d19d49a82fa986695d07..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 39760 zcmXVX<6|9e({vi+geNu|+iq;Lv2FW=CpH_~Zfx7O8#Gqq^zH9{-Y?fbaLvr_?9PsS zLe9KG);pnsnwo2xi7~sprey3}qgMu0B@znDa&>Nqe=c%xjWdlqpbrT}IPS~b>_I&% zA287HK|$@#zWWDsgCP2NFW9`+?JUNDy*MsmOutN-aJpvAEnMxz3)pei86*w_@iD*1 z=tZH8Q%z{l*zX;Nv3z+G&`QMeF0R6huq-N&9y?D4?S4vF1JI3W3l}F2QamCI_$4mz z{qyyQs6+NiBUSb8Pqg!J!+Xh*NXmmPdRL$trm+cBH#T2jQ(0-(f|f>yj$a@?K$R>QdLJo90QU}F(J zzWCPDO4K7t%Frz{75JB{KyoPgIM(049+s=sh_wSYs1( zeNPcVCM!L$@cL2j(4Nz~+{l3FQH;33g{>mX|0P%T5bFDwc|#AN^PJWcTl@Td{#B)j%DojhmcFSQk57S&@3V8y=r3UM3#7}g`5)7Js2J=0%)v7G5TbOQ~| zOXx^|cQTbWd#19gg5aeSg%CebRPLlL@3cvctTY~y6kynrE(@iFVNbKmHc>jz$=PVw+vi_x9E4M|G-+~9h@7R8~`*2 zEh=LMFcGA46bK14e%$P`$i>&@T9NRqMXsE=Dw`|55 zFQJCE-o*By?hZTgWDCwEXcMU@msdzws*D@V%uI}F3c42rB;ozI4>kEE47Xd4&>?s< zWk)m)zJ+Gq4%Un}{!EvM=05!zG)osYj2JdQ@sLnCDe~o3p1;&&i#>J|*Lo|aKv#6E9N9E=B zB!J}h3F6%os(87wCahsW-SKO-80b5aEeBLR)MM{R9Nh&OuTs{B`1rR*gGn%8XB^24 zIT=ux-qO?koB6SNOi`}PU49>gm}5%H4e7m*W!dfX3_8QJctu!o3L&H4k&3FLEBl3n z7v>g_vrsekC|f0a8xlnzNsqTRaEa+)0yZv$`xerlu-D@60Krw|I}l|B!Im+c9oN|= z1=TFs8e`0X-7tSwCE9(Up=6W|mM!X>87so0V3z5LV$hVd()C^xNUFJQmivSS*b=rQx)o>P?i z3=gD?qst_L+(f<7ps}2GJ#qr8)n}cY{877wriZC+@a_52BA1#@BXPN9oG_E zjuF6R+`6Nq%_1Y%c*EJYJ#(_EbnL7&QP+(jdUM(Q&Mm7m*C`xlDDx0w^YL93^m9Qc zJ;5j=vblR1{$a+^r8O+Y?+OWl2tX9DeFFv=DW@N9VMhAT*CYRB8+>f=dJT|-*YdoL zI())(E385?{H8*B7z#k3#_J%;B6s_saR4tj{Ce{XIgxi*b)gcLw!bB1BPLL#NAGm` zmf2*gBhe{+YAH>`oa-A@%4`k*?O}?sIS?QivOe(a8|)0^B?7@gW6q1*Q(G8Mzv;VF z)NeR@&IU+(%uo628TR?Xe==|IBt7ALs$2|Dg-XsKiuVYU*k(*l$9Reaq@QyO4@%GM zK27R2XG)3oiJ^2gSc2zC8!-p%>`b24uFs~N!N6CkPqItz`YcSdgv&w@$^!0?F;>(g z+U~bbUFu3lMM-?O0JglA98X&XxwX$_VYhkNeN3_IPc~_uS(c>z}@7=DOUnJc)L!vozIW}~&Y}`D3f$J|r?RJx>{yP^0{qdQ$)Bo+o zqL1$bdRxiq>l@~8dO*6C0Yx54K|}cIu1JTx67a|F;%0_lSBQll@2xOXier;)$>)y; zcD;=eC1%fhw8pe%JFu4)N%*{enJ?|zEtQ35D%e&X&_o^hpy9b%h_rdRzZ~dusp%)@ZzDuj{Y%m4*iVa6J~h{^ebF^8V}a=@WjB2OF!) z=jHB4%SlL)kDqEWM*klpKL!vk%1Djb28;V@y=r1{Dq8X80A>e;7f%6y;&W((65o$v zpdHDgf>db8*{!syk`$mqETRaNaB*|YwiWAWl&w@In7u$MPC9L=EfHgYOZBi=5n;1{ zabZ&@uDMA9!-Vcx6blpPQ-t1hbl4P3iz#5XepqwZlFK4tyTzg7TMaT(RiY|fa?@!g zGA21y_`X;X)C8Sd1nB6XSAWJN)YobY)hmIVz8edv9jeUd^hx(|fh0ntb4A32{u^$k z2;Snn#WF!apA|2ZP9VpMgRaUq_qURmkug*2_<kc z!J;(Y6#|SF0r%;j*IH2R_4>$`HNNJat-vxzbpgueyK;km{|ZJx#acyv7whQSo|K@6 zbsJpsa(N#yvPOAY=Nref3WetvWPGl1edR$5yTneYYI*u)^LWc7@?UhP4pS0yOKOKT zFK8V097I8nCZ&amkL4#dDAUD19yZ7a1xv4Zi10l2j)E=a(CMik@X_`hf=4m)oeD$r?87dr=FI>0MDT$R#PSvu9gemd5OcvUV zFXya&|p~1?a9;=ns9pFY$xu-o#bOVi%<^)MQiQM*D41Zc-o)^)S7nz<@zUk20 zcEl!$s6NO`>;H5&F5r)7X_gC%CD7(z@Cq0uYtchj)ddCc9*6 zHG`wK;0LIhoXQeEr#``e-+pDv5UVAau^pa&n;k}7H~I>()V@$j378Ts&q*lnOj^UM z=^Uvs%0SPfZevqTW$lGB`^FkFt7;~g9k%ZE!nMh(5EnJuFkt7*YPwdqEBknlSGbR) z`gU?FL0n?LUq9m_o85ecjEHbD`5fhTU3F39_YK71r@xSSQy8p3Pj04l+sKe8UN~tM zhmRkP`A54{M*Z$JS@2oGuL^dzua!5M=#aNyAB)%Q{C3+$uRnL-;SZ%N&MGQq_UU9s zZGT0_7P+F4&e}ok=vutCDVW}FyE$W*C=CC;I(uUA?6&H;A@mME%lNWADvu4r4}Rjd za35sJqZYDy>-w5VZtVVxiTW|UjqYVfSy|9Q_s0V~zvf`G9wDf?)U8YVb0fZ$DvE9; z-m{lgqScyN&!@NF@xu$V*Yv_Zv8fby#`8%3fotcH?pqQDRM?TZD?(24NmrGhpkE1F zVz$z)Oxi_)5FJUju9^R%Wm3_=ADZ%CVa1@kOTB&~62z_L3CrNtyTrX3KXNbnJWOJ0 ziin!`ouge9SuH0)%%~h;!=6B*=<@hSCL>QPlqarPV@EHPH*(jt-My^AeoqLcMKZ#f z$zJI*w%QZX?-!HgcT90TL5I`#R#_7EG-)MC-fnSIhlvEyr*Vn4j&;|lUL4qrsK}rd z!4*HC5>x)Q64eYWqUDkSSodAC@M0JKYQ%SIwOPEv`{s$n3*pR>z1yAq#+r_u z*rR?(-TP~!q(*S9zxZv_s|&tbveIYQfg)%xFfc9c@lzZ0yceI_nGuL#sKyGn||%zP!^ z-X-~Zja=v*Deu2exVT_Qu^-&wL1HZD2ommm@EpiXe}kd>OOQ&UH+}gB0+0F?-%it#F!fdbTOCF#I~k0I3c-V-g+W=etAMi@!~jv3hnMHrK#`0 zk5~TA|MQiHcJN{v!J}*(v2E;X>YL)Zg4d7ix_W-g^{l!E@*VQ1^NVSQmi@f7I8^O$ z5)*16Nx}0*k@ea1{_nGz?I+4FpfCRw;cq@8y#{a)5PYZ*5Xy2;(3r{a5?IOaWU`=S zd!>JtYxHk=e@7}gi^LFhQ?LiBIbt~yZY+j^JX#DpuD9pvj(h4K4{Lr5)1#1QJimg- znIW722;r35CO24Q1ktRAt=!Mq>+D?Lt69Tc5QH{({KnYvTH-Kg=U^o+p{1u#*S@<{ zH)z*gkhn95k zQO_FT_#_Zds1lyliV^9iM=O3R8{XAP9z%okLVzTPguIB|`T7Ql8?piLDWJ-2%Qb2P zhAM6&v|mPc{Azz}?t5x)T8(_j4o`$X$!%8=ADebieI1;$9udGs1moFAjexGQ7|3T6 zP*o8J?_Lud-VjjHG-*F`>9?PS2K5gq1Mjd0>u;O7N<_j+Mf)?rkWmsbM%l&#Cyu(o z#l~GfmU+#qd-prLuAI-7vYZz-m+!bjOavk(jzyaT7crme#QQO2{D>EGCGksDRGqO; za7Q3ta01@Y-pLS@R;q^&r9iZ8()Z}nNgjgei+)=ipA>KXUHD9#kGfD;6*>QBN<~EuC$iK?WmH zl`-ec^w@NwYMxywt;Eho@@p=cN!*3@FQl)pZ8|lN&X;N<*@J#xv;MWT;+mI(xY85l z?}?Nb@}k9BZ>bHK!l7G^|6$FBtVT}mpT|o7KabT(sRt6#-6+v3(F;`%21Cn1i3fwm z-1zNqJX*~>qR}W&57?i@kkiG1Bz@s*x%MKHRA&y2>~A^OekW{}0e@d^k@_gH@r3fS ztILEcd26qcsOx4bUu!d!9}E4Bbhg-|<1BFQgPmv@`t?OdAU!#|Ngw=M%{qTyFtzF> zDx(6Xk3n#mXSVRHLY)0-L#Y+?f47s&(f6?1xQ^8b2i-ywN=?yxXo}?;;FV(KV~U%) zc+`bCgIGhkqNpmOS4*jIOQRR0@smy%6PFm-+tr)wua2~2XeUePkM=f#@?2k`($mMl zqk;wbxnwGfFPTylVn09g2J&ln&}bI#HP~DMu^@wfH#n(lqg}+4pC<~V57@Xn;jE+@A2QAcOeC-^rji$Un0& zFd!)YV!!qj_XaEhMUVF{FQ4&rpm3~|vJ37B-l>C`+zi=Bk~N8HWV^ag2vK|I(lm{O zdSaVi9iihR)X6MmmZ(ut9PheE$%fkH6&%n?~gB*dZXhhtGRQ8K^Dm;&k z2MPHO*cb_#jH1D^5wa?}=u1$o<5y;fE9d&_JLRepAo#z*Y9++aU*5~3oZ)R8;Yr>{ zuBQcNr|LGd@*r;Ta}k~c+#h*+0ADk*lNCd{Nq@k0il`oysGph@40cIJdW%KPVMMbx z8M74~ZE3b6gZ`A3GhD)&V;^gS7ktr>cL1!%dceOmd784U#+JA}ceH%TnPbv9to+ob zFU-e>pYX56AJSYGD+>RQh` zv@SZntx^5R>-!^=L06!Hql@}4Ae*66VZ`eE9_xp7@QvDi_jFx&OmM^P0scyWm;dDC zxtw>nGm|;3qu2Mo#S*yDi;W-!ZHBBI0iobgB)aq)OhSgiS7L#sayk>N{eHU<#@wl(b0)X*Ow8ZQ2AiqSbsY<`h~RDzk~r!1rSv zWMkO8(z3uYi23+$Aii8&68ZHL0+iz8S#S$AMVZWQc_sKX^W*JfG~E&6s%YkB|M^+s zzGh{AB+;pJqs8K(DbvC$&T(C!NkGf9tCrLNQTOKCoOvEx2WTE=M1{o((!O)_^4k)} z?h?_}xn?ohP&b@zmrO&WckV918W*}q-nnNH+G>*?S@EyTA(SvcIri=Jt7dnF=diMG zI;`nfQ&$kj5O5M3&_O*7ruAOMMjmXz@60`PYVDMgooxq%>g_%i7@6+6-D2?kr{J)_R{+kN0r;WMaVylPq55VgBF}yc!LI zD(w+jSS{z+f`{vm!yt3dFm#L2aEbBT{FlhkbfppV}RxRCxys(W-+mtVm_xtu&Pqm{TO%K8Ys{8#ZctXj5kL zbMkqZ26E6RNK`WCmW9uhoX_wtzfaYCh$o_{jalY=iz*(aM-Qh8_+JzCrGo&UEIEk5 z1T?Eiz=}398cNBLfRW!9IawKAJkfZN*A!d{hn7kw5hy(zw0Uu5W_q)c=m|v7_$A^M zolE!F2X&*2b%>^uK;E$6Gji|$xlSzn{^5!W(OJ*5clGCw!27-gu3@rbmp}8Bw>>l0 z`Zqd;;`smzjDrp;i40)0|I|mD(yhDD6v)M~H=M4lgqpE zz+KxYhh+w?AGfYZAWtwknt1`yi|m~FoO00WH!j{!+>tz=Lsm)xJLc>B($)ObG(C2@ zW`H-tB`DBS1pX!u_nP60aSvS2oQ=j0NRthUZ9u7aJ4kH(eX|S+vtHBU2wk0H&GtIT z0r$93ja)`&Ov1^$fWdd>GVbgyUY0~|o*DV0j%1i>1>9`*M?r>x=l+bN1GefIeUi^h zWs=z!l0{y|TOOzqssFgY@+l`-^^f~A|8c*S$pi$CPmc?vWW^`=w^N9SY~Su?Kzf_s z+AbU!3wZ{7&J`OSp#Im5%rHveQ(8a&WcRd~`N8h`^!a&zj}zFLVgB6M`?v93rq0Dy z3%aEzUsu;hrB$@|*hj!)uE#*Aobca}1~ z8R%#l0n0am1#r%57djl-3U(^)lLWlt|YBB6J{WVXo|nKsz!V+0qlBQA_pfQDKk z^Y!0t+@-W{O#;-!zOdsRsT5CYmwFDdH z)cO%BD*!TvX_{nr18uGxi9hm&Aj7XEQ7_L$Xt-i+Rx6AWdW&xTq*^}(;lq;FN6RwQ6w?YBnk>bjWMC9 z95nicqa-knC3I$S**_R6i+lR)s5%sP5M5&}lWOUr<6a`1-+hz+N)h_HKrp6=2U`U9 zO>Y7P{AXxe&TzX%aO$^Y_p6nSX-Z~K`UT3qKe$ETEa~P;$Wa**3{5KTw%hosMZK?9 zB*p1axN-J3Eod^1&KWmQvOrH8z!InyyXG4C6V(+hZ}_AlKLlYm>$^dZ zCA&aE!s|e~ZGa{95NyoHrlqltqkHwcsng71a!b3F=4x&({fLv%ympl?yD`{CGZCu; zMT;hiYq@wU_4l2@qP{W0~3w_ug*fKbacUYSwSrHY`QZ{v^7fNOa}kgUw4T^np)u13-LmbXlQ{^8d-;{K4fZ)_-=HU zwi|g{{2%{0mQgg0u|6l7`R@A}bamr4o@5=(8pV3~5XgXk`wnHnz+Wpo)@W1no)N~v z?qcR~I_JfkvlU?-Dif;rcURs~Fy^Q){88Jj7AA*`)xNhQPf0}mEnqgilP;?r5V z?wLeS$KNcbPfX%$d9WKB zn)6^mj7&;-h$<_IzAP&YD(Y!|X>Bi4hB;w*yWJ!XB>`k8V&6T(|I`ng2g>UV8UkVg z9wHf1g5Y2N1ehGxik=MMMhEGu0WC2D3^3N}p*d7An^SNlNJ)wV7skS|X|Soj*j97M z3a1?@Xs=zAb`nUIsg*5)RQ?9G@`#CKS)Xe#^L;>dU(B9LWaWA$8!eb^9GmR^w45Dv z-MP1)O~3`Kv83Qc9ENfii8%!vD4(I?qf zuYt#ZOS9U_BFoaFMztSt<_L58Akm1Ggpp>roX*5a$lG2vGPwq??(IZ2QxefuH&PKE zC|LJ9JF7C6`jVKNaYEwt`FY7pAoG`Rf1SX;wd(}U54-@mWgf9UmivaT3NudPNh_O+ zS`!_CPBTozs9XhQ0R)f(IGKMU7h@4qkVJQPV;@gf6lV~jh-RVzJFnO-F?C;kfR$mr z5?fcL`m$Ix+x(P1bL*g^+kn%NQ_fj8<42d##qCAIcM_0Y)>0W(ZhKF#VPGE|N(-j{ zE0Nf7$%(gk-~#$309ri%EQLH5yd{r2XPcgs2ewxVB(X?wV#%qC`ZYBNYbtEAIqsEO zo%#ZHm}SA!I1d*@V#|1631k}U&Cy5M{v;JxA4z3o6>5AzAD;PCYt>e5W++qaVmAG* zehD(&Z?cvvT7Suhqc036N(_YajHr-pkd_M~Hjfd&qH1^W z?hsIio(X#8P*%Xg#cPp->@bFF6p(^wJS0AXZr{xJ#GY;e{1Xc2NDodi$BEKjC>{1E z80Nhq2k8gU{@2JAWEr6b?TX+q}pdO*Vh2+4pt=jNC*w>$W2Vv{t$N})!` zgkyo#v`N5-e_)X7)fgEzSFTn7NCX#`!w02bqo0ruK3!Z621HJ3oH>NZD0q90eNol_ z7d+uS{{xWPkZmA1!1%O9zhxmUX_N7lN&Gi0aF#Uuy((OLH{axg1d)t^SYw{^sq4=6 z6of`{Nn+YgS^vD3R6j+?f(77n#5m4*5@z|fYt3ZiWpT!~?KVQw{{`02#Pks&{Y;wB zC?d{m6*XZY%eGc|f=JO_QuWjAfl6rI6Y+ju%}*1lNi>M>ln`m26MbyTwqt%dZys-h zIizfxe5#T@`|d>S2o#!=7bo%*tg%P!hy!^>GYsS2_sIR9P}Tl084dm?RI2d*o9B%2 z^Me%R2EU>C+b%EZ2>%{k7DFj4VYR{vjv@`lLBfJ57`10pXx*kX=cbKVBRS~3Aq@@| z?jxa6MB3>BIPUn~TX^<>gnA$dP388?+1inEw`u|5DUw$e1b?=`1Qz4c)<3Ek9+Mcz zkCCmD(zGw+&cpl>!&{`QeK(RfR0oNM4M5~lxtSJdL^&MheFnhy=_kaRANBrcM2d{o z)vDx03mNOIc$1wOs3@6mK{)ek{!C)<>Q_GpLfvXO5H2jf{xPMXPzWeb1<}WroYKi* z{E%dvv5hZP@?c@E7fLWav;8p=(8-_A;#p5q&y&_cN?*U7c_vZY1hS6tv!l(*YcSr| zE1~N}qq)2kR#)kFfkCN+%=-IGRIOPLw!t!IU^M>18T3N`$!xR5<7e6*(Ts<&$WfcM zb(uc|zgF(K({LBrJTuL|a_+e11!HlAvC5nBfBw0A$rNAhp9`!0zeHjl5H967=8 zUeWlGYsC#!S-qp&_T9s$kE^GN*}nl#P=VWR(=6_XBWItsi7K`62vul!5vRk_0)?BY zmBuc!^)=$dOz=tk1DIP_#SE_81?gcz$15N@2ebS!1+5{9W!1ugDg-eT_y*TmrX8gg z#lP9028&Eer%8bZu}p2ML5u;`Y7CjtutQabq$g@msy84ED{*^mFe|jH$MpO#!XPF9 z;a`s}i^7~iZmyl{#NbdGRVl;xw9@5x8)80@NrUsr!xDt~Hx-3vr2+ePsXl(i zgPh;FCu11ABkhd5+UAq)o5)Fl6io7Bl z<9_r*^_H-^|FP|`eGsf=p}if&;thJ-^FC`%?2=TsdQje-s#jwxLL>+1*Ot44`?gS+ zCK>aE;?y@o>DI^Q9z;}*yHAW~TJdd@!;y*4)0+64D!DVyUm25Ns&vp$*?`z z{cu3wN3ub$c+tVQh+ZSJ_hhSnc;uXAQIjGJSF%7(pJ>Y>d#^4E?tU0cx&fJyEqYAf zy_1`Xo{oMhUJOEr`Eo2$y0SzF@`y{Yl&71)V$a=)&-P!tW*{!(lk$rdbBOAUz#F`WUZ!3JKdIX{Zzk=q`y1r23efs#r*xP} z=o-uKvyO{{kH?>!d1<9#LpWUsE{M?{s3g(QuYHUO*@$f2o9X zclF>YCz@4hU2{+ctFzud;Qw$FR8jdhS(Lo-oMNgKcBlY$Le3HC3h_Lt{(zm@;7fP) z3!(E*^Z3rwsV$|xGYEPkYKugLpa1#uPpH#E(|Dd;d)Nx&?j5>Nn&V5Twi2#pf3A~; zpX_u_>9QhHsE!y3!O=3ipSr`@ukRZdV$BofPJRgX`&7!OsNu%ldVvqil9p&WlrPys ztqqu8l0J#5OouT)+u~C_(W1h%RvQ8kdxr-Iey?$a<(ceHe~yBR@a)ESM*qAQTqSmD zW1lNNF3od$q0;+wsChcmuLvF>z24ng7yn*Mot-gK3aGy(vocpYyb&WbA6t0D0`qH= zl+~@`1q}6s^NcG?IXoL2I(Nmf-*fRF%Xi#`?7O-jmDL+A*uCSS8YZlcTz`&ks;&9e z-P&H9&Rop$reNz@qx@|t%(=b zuhx#O^C*Bl4&DF>s@JfIn+!KP8(haUSp0P~NX@W1p+3sTx99pe-Tm2erd^X?+<}Hm zfwO`)B>$DCcHCY_QWyvb&HpP;@M8KPQDabZg2L?kC*dJPl)*2Z+nZ6kDCu-9Emm}9{5BO zHrSUEm1A=DW+g}jC&MYYo@UZMCN50=)yKuyJv07p9LXb#2I>~hOq1H&#NzRwT*9%G zW~L8a;i_2UzFG74`ouMPUGg&fk<+B?6N8wtH@G)zffDlvXJk<$C(R|rc{zLOy*8)s zNxZzADOS3PKNl$3OEJ{SVRWgOnNlmd4O(DkQ^~KDN>cIKA@qZ$k=j!t6RZ6M+etNG zw6V1PD{E?V5!^gHX2Z3`K!Fe-s2~-ly02~~h)Rw&3aRZFYdhY{nWyB|2)wLrUA{91 zfA=6fp?xZ4!q>z>Le`{kGa1(?Iv84a_O-^Hy##>pIWe#&%)I;QrE3JYWDORAMkEx5^C(mE^>j9JqP z9rf$TS;%X~d&|8dejZ-?1$+s?F(^vzBcM@gqEVl#uOBEVFI`Vtt~1x!yZ=I@B!55e z2m*iB;}Q${dAUCXG?kaPwyQ+NNi4f?pleLqC@f;>vd5Y&GdL&d>Yd1H=Pd3wsw!1Z z>UHZos-Mp{G#0LUlj~GbR>?9}AdqC|33*E5QR&;dE%t29xnnG)rykK3n5c7vxXQ89 zQFc$(@EIrixgAZ7Sv3w_OOkl?lyxYfz9)Bg@_vn%N{zK?1-<%j&D){N9o!Fi+m^YqPNW13jp0 z*ZUeUe!~f?W%sNDvCVTD9xC?5YyZAK#0cVZg>K4etVNT_^)1R=b?p!0-~d6pqfGYJ zB(fM53`lmm2?qzv{|mT9pX?MW(&(k2rGD@rZD&&%VhrK9UryrB({eWOWjeF6&=r9i zcT3B){e=<3Gdt;W(`z(5`PA~XWxCoso=&WLkOgIwQ8LTqS!9Xig3jqT-!D&3^_^QKzQpW{atjUc{Ho3bSFZWm z8D_DsEAE;0e^8rEUUQt%T^4u%Jrk3voqT1reivh`rxsIC8s?ek=mq0}=RD#V;oQW8 zL7T2s8@c?(Ff5U?AJPc%&Nsuj1~T1|o$)uopv5ejk9vshlY zTG4e>6hw~3STeuX9BXw~I%{y<#pOoLOb2o|L8~$qxegUiM(f$Wh~lw?IzaF{{)KC- zO`>ibG+Cw>hM;4Il|)g3CK)UI*)9}58vIwl~kadow~}Jn-D_LU-C8XZ;WS_m+(D?7>qZqKo|I5lmJ>PlLuHP0X#9>Gr=(^ zs7v@BE@hw)8h*C7Q0kUS$hLIK4c`(J9ZFiDR47MzkTaxC;1_gvTKKo&nQtaJWYTzc zA)e<3w~-J8qx6SX)cI9N>W}XOXhI3SK!0dm=4#Zb^IzJ;j_hR*o@8G0#039Bo?LpPR5Gnx(u_@x*49E?0B=a==b*Viav=_$Onyoi@ zr{W2QN1b5)Hmhh7GbPB7oS=JQabSY(_swt7VY#Ap%o%+_IC!)GIh05-{1nPgxyu0| zcO^FiyO2^3>+ zZ)psQJPli*M6>vcVnWfG862d+r(8jmEKAhI&Ne$PpcTeh$xZ{e%jv@|*UqL(s1A+& zbfgTcQ|f$E`BN$peK&C2uc0=AX zPEuRx9y^*z0UiHv5T@Xq3^43Mwt77Ru;gzy`A76RyZx`>Pb9TpgG)@8HarO^^!Nsr z(H)4nP)0B81DL+~S!S??yQbKD-v^Wi{L6;H!8_fV`zOcBTY)%7zGWvs*CN63-}zuc zF3^eaLPx5hVWjbpGUYh?3eH?z*jU)1MEJq)Cde_7I{&+~qYgcy>MRa8O(6K%}~=kqZV!ZN0d9grVy&c>E`3*%46C-2dvN zBp#Jo-HUk0Jr|UsPF*@~6!88K$Tr_`(F$T?MkISrN&1j9aW(ys)6iazAUuo-cCZhpEa&(szLj>azv|aQ8Uxi4( zuA{7zFukZ{$-Y81&@nKR#Hq1irbCxycPnm1TP@7K5(+X6OEvhb0B65tLm?`KSj@R9 zvTuC-;P`j`ERVQouIsq`ucq;n&scGOd#XmeS=*BX55?-hh_I=?F1a0@I2BByPuS(o zUs3+H@Jp_i`l9+*J&!1+6*Hc&th;n>XmcIj>&Y7Wa_H4RJ$rw!>Wi=TuIld6#(Nx{@ov4oc`*p|Lph)FJ2{($Mb3yo@ zHFi~Cp=O~VR^;E}zamO=+>>=ph2@&I?BLg46^>tZ<_ z9N=f!umL~qr*KPmFL{~bL4>>Xp8j&m0%)~+1^L4$sFM~_83e{#$gyEuo?@(~4;L=! zPZMzhViAj$Ctj)bB9E7!9v2;$@cdnVvZ4Z;x1sQav!zys&}7akU3~o9x{SJoj(+U$ zBl(;kJS@W+qgVh=;d*+HK1MBdE~uUJ$b6Ue-3PrqT`A^huK4X!(B-?u-ewT|AQ&h) z01S%a5c9}+@*e(`tN-0V7ssO5B+$6;(OwrCkQ|GO#*s9PKbSAUSnn+!srQU_^VdRD z>hc<<@cP;L8HJ);n%Tt2ed46+kazwB5ROD5Q{Fa_K!>U24xp$K87_|#F=JC^DHR)} zUYI~Kb4S4fatv!V+{mkJDmc-K+;eK7jWf`J@F#d=D8q*1^A+Wm2nV%; zo!({rUncz39*#nkozwRB-fHL@aCr3_Y(EGGhpx69+#x~iyl>4qdDi77K~1?tL*7Ji zPRbNP($^2<-B@6nF>sAN?z|G)f;jCq^t|# z3UW!SF6o~jfd$MjBeFI8N)1m#*dy!Me@a?dZYdM})W6=szEi`V!u3pB5`P5xmgF^D z^XhzOv{ewC5`GwEZNJt3eT5Q3ByY+26Xc12wsa= zXXAFwPu&FE4>FP5@FWYWN5|hV0zxZy0v#`w42e7w*M^moM4##2b$Ek?9LZGGz zdnLf>uwGYRv@`t}*-)x&h=7Fl555RP#&s^dE`fMM6jH?*m(YZ?WQy~S1mb0KUpm$d z>EWLy`XD@5Q)Qg3B#z-?b0lymz3X`P(RW=+Zc1kCFnPr`g1E~&yWMQJJjb}y_bw;D z$)g^6tR;3g!C&VBXYj(`4{gmNyg*sG%!rsWKm6pp0QQTW&q0g_8g>ijM04qOu~BG~h!d>PwPgA?}#XIPl-uq}ZD0 zQy)GPj66Wk!m1)EELbE71oJ3rT3yZGd8eKcpt0?r47P3Ck$<{{ovuzx4bB48_;R46 zV8B*{6njJ$oCve2I)#5d?>rMoH&wk;0Q*q9^3)gu4(Y%Nr81wU0`ZH!9X(Yhn92AX z^XPE87c<&IO&{JAdbGPo%idOAd^9df^uYv*Jj*w#H|0dguPeyoBTGJj*P#6Ec zK^FV*pn<{v9W{*M)F8q6SVBtNwicj3{oo<@egmcXGPw0+SV zIj(*57*d&r!NCR==yfz0{1csy7MP@3musxOrUIpn((zZsVq}GdF0PGQT~XM^$ju}V z+OrOe>*mc3`|ZP3!A=ML&Jw(eC*j>xyO%H1d^bwFo-HWby$@LAIrH-cV{F>%g)&WzrI^gVF|I9l_2Op3stxl3>ai*<>4mYNTe96afQlV#OvNtN zN6gAVVEJWtr=zx6Foh*+`R2T zJY6g=SdbWA@tP1I?kIQmqo8H`{{e15k-r0njJTsw1ye=J92zn#&`0KA5K)VpL7cVB zA7-LKpxH`1sT`WP~tZezqxYh-U5-2|B(GwO(c&gAQ2 z!FL_4_mM^$uovX}^iL?-DOv|q(j_YMReYAtR{N$r5IknqQ z3uvLtb}=o3#}6ila+U$^$40j1oMCueGOn_apLUCjmeU@%fvpc3eO6MPsBVb>YU|tE zRn%7zWb&878uc<&!ctLWuQW`xPwdx6fBV4_*qx^B_$lV%?sjo|Ow08)$b69AAuIP3 zR&;0BPxrdJb=L##%o!G3DDEN?OjST`xAdVjF5;&_7Y~2RHq3UXw}R>V<;Yy!C*|-% zNP?w0iH>9({n)l+aU&~g)+ohv-4uhpIanZVl&ohEMB8;_<3!LggIV3OjUf1Ve<{n< zboFcX4qN6?eIR8N1hRZ&5)zs>QSd6J8)g{Pg_35QQdC^ zPiypHrfW;(oWA-I$+9!AFIs!pM-S1jFx5@1mQogWeauG>(#NL0?(s4n0cFyTr!oAG(5_*c#iA4PI19U zWAqY&Kr(X%;kFDnoVB^Y3&#HveOV~J0-FQ}O$%`zkw|!%DKys^SLO6I;q==xDCa11 zvnjtWl$YdZgOBnee_)QBqR}^zJ|?XYHPO!&O5W%u?S;t8D>2D;5eUJXOoaA3M z5sY8VrBO$Ba(3r1e^?nxraSHsC-?#VgOuQZEH-*GvWG_hjJ-!Kv}-7HxJQ=?fq$hR z`siQi-;i~R9YFA?ZU>W7(zJT%-c5<9_-tamSz1f8)G(%Cr#? zKa&c7j^2=?-Vl4E9jz&z2d4-QT4oyl_jAO3a8T8taL{p0e;jP^qHm^oX}i(OW#T6& zEDMGai>+DdCM2hLxqMo4D!njkAR3aM_Qqe}n8p5!E7@1YUamq=3xB)xfcZ?VS8JnY zb~b2ic_FS-+R{s>rs9=rd|b`7r5SJr=^{iXa#Eo=LuoI`$J4e7Ltety_;@j2JMB^6 zUdz__I_S$ne=1E{Mvs~4!E4RW%h>1RrF?xg`xaKRe}su7Lfh9)UJg<$$t=?MioPz;-ioq7g3wO2+=^KdSE^~Pr!Ved%R z_~jPeBd<=|ID55IPo<&=Avnt_zR|}kdG*2y#xtcHf9{aNC0l3Ny96H0WmMg0+g_M} zO%pfQBE0c}S(re}Z6ybCveIXzyxjT=UlVf}YSNpR@ftDlP44qoRZuwTkz_*Lc^w*v zf)DrNVi_;vtClk+s7pDbRiYhxYdhDw*p& z#+x|of8%!EEVb@AncY(CcIW1z@ojLw!Uf#`-U7c!@Sv_4#k-h=?)LL;-s7Xq zd?$+E{;hj^x_Wj5`)tXHs#*1NRQ04V5t7MVf2!%@eU(rMo;v;x1I@A(EEqHf!VVGH z%Lka!!Rakt(3H+t&mhy=2FjJR!^OTv`u}3J34$oNL-|Co)InQ=d(>AWA+yD&g1Jel zqpedRg~FflBf?l%(VY)+km~WmaH~o7ZcMca;yC-j^a<-B)e4qOu>=<$6i_PRSbqGBevOON!M8(EN23Qbo`ZTsYX5F^*-y53jEHRMrUE zXkHAs#|N(v5bE#``}hPui13p2)+1gX%=iR9DUN|xkVjq(h!(hJ{4fmID^`=QiOG!7lS>aEL%W#j z4%2kR&RMre*;Ipfm4*h9fl#s|%@?SV=`q@bNr>rXYKz5oU7)p$#_Q&u3$%&pRYGPvL-Sh{1oW<^ zP)nX}+ka-_m8KWKmiZa{wvuOpYN<@4fJUo`-lQgt+BDic0a-jQ77+f3e_jU)jVryq zAmAFRPy()OiXA*SN?V)HQ)kP0+BQx*V%^Q7bVt*9id=u5dh&GVS=A#~${W5weF~7M z<+gF^iwTE3-PO&JJRR7Tr~X^>G!XXW$q1L{X*gWb)ZB7?ou{t6u40r9ztBBSW~}zU zrcrV(DkfF5j?&O#jT&odf3X^uP@Ni=(sDHh>1}FUMQhdQs=!Y?0T3F|fUBV#9dSjR z_chl}{6I4__pUs>dw=bFdpPXjaQPU$KTjWug)7GC!B|)ur-x!Kqx8{H`b3^S1!FX| z;D1c$K9i>>YoF@R)32QqO?*N9{>E47`NwES%ggk9o?eV?siAK?e?=8%Xu~+=W8*Xy zTiPEQrSUvnto>@9Ua70d)2n&<#wh*H#YmkN_MD;D3gfAkSf2hcTwc>aU-CkGe{xG@ zN99IuU3qh!{vvj>uk5oF8>8>%>F-X{_9fmGi+v{!cIX?uEA)dMi|Fsul_#H|swLiK zCr+NGMNKP!GCIyte`MK-CEh&!Q=Qg4Z?P{=KLX`OZ^xO5FNlD({~?0ZX?5jI=cu#x zKlAi@p8h9Km(NDd(E3R64x{vD?L<-f05hgd>h>1{JP!aa)I7?bizRF>5hMq%I*-g? z|I5u6X@wY&L-d*&8)2yx)S_S+1#Y1>Itf_DhXJppJ_XAtfAC$@uV5JkK1BB^SVn;@ z{0c0iz>m|@3YJmeXX$eamQmo((-##iqrflXwIr~N0$-%BD_BN>zeC?uu#5u#h<>bK z83q0cmnm3Azk7)ED(FAlN3#8J? zIT9rN`gbQVe=ETkwqxbB$rTN+>FKN%s|Ag|&7Mg-0{${>;OYsY*90~w$S z7YSIf6f;54whxh-=O)5y^qT1Mlfr{ zSR)_T+#||1L6{2IDBCO?Vq?5~|4VRHiuE)HxNVHr?ho*K8Ib1!d~;}wx5UC8b!dC6 zr_RHpeCruHir_r}e1JRL9p!bH-!Ai>OSC0)iP|N=>dN~OV~C{caGrOB+>q)KPGL_d zz+E`!e`S=fe(rAGEm%(vezO7?xz}!VOgzS z7Q|-pD35rPi=05w@VHS{M69NHZcS7n^Exe4E^#Yrcr9FF4P4?jc-p<-azEVRQ8>ht zf1n;i_5U_}@`rGqH>rxzDRCFvsh1Y80ooe*2wU*fr^%=4+9@fF$~zxc-idp6EAXR5 zFrvVZ7r|W${A3aAQQ*lU81^Xm(Mh3PraaOc^R e(*aw}>J`&Ep$5$o2mVP)i30-M3~u@DBh0 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 delta 40115 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 e{Sg5G5zENPNVU~b5#<2@M#Os+0czL&{q{eZe&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 +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ 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 +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -: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 +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index b294c8010..1fc2e6a29 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -2,7 +2,7 @@ coroutines = "1.10.2" kotlin = "2.3.21" ktor = "3.3.3" -logging = "8.0.01" +logging = "8.0.02" mcp-kotlin = "0.12.0" shadow = "9.4.1" slf4j = "2.0.17" diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar index d997cfc60f4cff0e7451d19d49a82fa986695d07..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 39760 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 e(*aw}>J`&Ep$5$o2mVP)i30-M3~u@DBh0 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 delta 40115 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 e{Sg5G5zENPNVU~b5#<2@M#Os+0czL&{q{eZe&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 +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ 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 +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -: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 +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% From 3c092c65e80c278337afdf2c2f9e193d64f640d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 16:41:55 +0200 Subject: [PATCH 127/187] chore(deps): bump gradle-wrapper from 9.4.1 to 9.5.0 (#747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [gradle-wrapper](https://github.com/gradle/gradle) from 9.4.1 to 9.5.0.

Release notes

Sourced from gradle-wrapper's releases.

9.5.0

The Gradle team is excited to announce Gradle 9.5.0.

Here are the highlights of this release:

  • Task provenance in reports and failure messages
  • Type-safe accessors for precompiled Kotlin Settings plugins

Read the Release Notes

We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

Upgrade instructions

Switch your build to use Gradle 9.5.0 by updating your wrapper:

./gradlew wrapper --gradle-version=9.5.0 && ./gradlew
wrapper

See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

Reporting problems

If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

9.5.0 RC4

... (truncated)

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.jar | Bin 48966 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 2 +- gradlew.bat | 175 +++++++++++------------ 4 files changed, 86 insertions(+), 95 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d997cfc60f4cff0e7451d19d49a82fa986695d07..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 39760 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 e(*aw}>J`&Ep$5$o2mVP)i30-M3~u@DBh0 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 delta 40115 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 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 - - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -: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 +@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, and ensure extensions are enabled +setlocal EnableExtensions + +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 + +"%COMSPEC%" /c exit 1 + +: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 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% From b20eec587bf64563704406a26c02dd2a40cc9a5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 07:50:37 +0000 Subject: [PATCH 128/187] chore(deps): bump mcp-kotlin from 0.11.1 to 0.12.0 in /samples/kotlinlang-mcp-server (#745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.11.1 to 0.12.0. Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.11.1 to 0.12.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

0.12.0

Description

Adds sampling-with-tools per SEP-1577, JSON Schema dialect declaration on tool schemas, server-side tool name validation, and capability extensions; fixes SSE reconnect on the same session and stops swallowing CancellationException.

Breaking Changes

Sampling messages can carry tool calls and results (SEP-1577) by @​devcrocod in #718

SamplingMessage.content is now typed as a new SamplingMessageContent supertype, with MediaContent (text/image/audio), ToolUseContent, and ToolResultContent as variants. MediaContent still exists and now extends SamplingMessageContent, so most source code that constructs sampling messages compiles unchanged — but the binary signatures of SamplingMessage, CreateMessageResult, and related types have changed. ClientCapabilities.sampling is also now a typed Sampling object instead of a raw JsonObject. Recompilation is required; consumers reading sampling as JsonObject must migrate to the typed accessors.

watchosX64 and tvosX64 Kotlin/Native targets removed by @​devcrocod in #727

These targets have been deprecated upstream. Migrate to the corresponding ARM/simulator targets (watchosArm64, watchosSimulatorArm64, tvosArm64, tvosSimulatorArm64).

Features

  • Add $schema field to ToolSchema, declaring JSON Schema 2020-12 as the default dialect (SEP-1613) by @​devcrocod in #696
  • Validate tool names at registration time on the server, following the MCP tool naming standard (SEP-986) by @​devcrocod in #695
  • Add extensions field to ClientCapabilities and ServerCapabilities for advertising supported MCP extensions during the initialize handshake by @​eritscher in #678

Fixed

  • Validate the mcp-protocol-version HTTP header on initialization requests and correct DEFAULT_NEGOTIATED_PROTOCOL_VERSION to match the spec by @​devcrocod in #697
  • Propagate CancellationException instead of logging it as an error by @​devcrocod in #706
  • Evict stale GET SSE stream mapping on reconnect so a new GET on the same session is no longer silently rejected by the leftover entry from the previous stream by @​amr in #716

Maintenance

  • Add kotlinlang MCP server sample with search and get_page tools by @​devcrocod in #713
  • Migrate detekt to a buildSrc convention plugin with a fully explicit, version-locked YAML config by @​devcrocod in #729
  • Add Streamable HTTP integration tests for pagination, bad-request handling, and logging by @​jskjw157 in #613
  • Remove unused OpenAPI Generator plugin and configuration by @​devcrocod in #683
  • Remove Codecov configuration in favor of kover by @​devcrocod in #691
  • Remove redundant JUnit XML report configuration by @​devcrocod in #728
  • Remove IDEA files tied to the incompatible detekt IDE plugin by @​devcrocod in #714
  • Add Node.js setup to the release workflow so TypeScript integration tests run on macOS runners by @​devcrocod in #684

Dependencies

  • Kotlin to v2.3.21 in #704, #723
  • kotlinx.serialization-json to v1.11.0 in #687

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.1...0.12.0

Commits
  • c339d8c release: bump version to 0.12.0 (#731)
  • 64804a0 refactor: explicit locked detekt config (#729)
  • a238fc0 fix(server): evict stale GET SSE stream mapping on reconnect (#716)
  • 66d9efb test(streamable-http): add missing integration tests for pagination, bad requ...
  • f434fca feat!: SEP-1577 sampling with tools (#718)
  • 7474a33 chore: drop deprecated watchosX64 and tvosX64 targets (#727)
  • 12ace76 chore: remove redundant JUnit XML report configuration (#728)
  • ec606db chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-serv...
  • 1d93042 chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-server ...
  • 9817b17 chore(deps): bump org.jetbrains.kotlin.jvm from 2.2.21 to 2.3.21 in /samples/...
  • Additional commits viewable in compare view

Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.11.1 to 0.12.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.12.0

Description

Adds sampling-with-tools per SEP-1577, JSON Schema dialect declaration on tool schemas, server-side tool name validation, and capability extensions; fixes SSE reconnect on the same session and stops swallowing CancellationException.

Breaking Changes

Sampling messages can carry tool calls and results (SEP-1577) by @​devcrocod in #718

SamplingMessage.content is now typed as a new SamplingMessageContent supertype, with MediaContent (text/image/audio), ToolUseContent, and ToolResultContent as variants. MediaContent still exists and now extends SamplingMessageContent, so most source code that constructs sampling messages compiles unchanged — but the binary signatures of SamplingMessage, CreateMessageResult, and related types have changed. ClientCapabilities.sampling is also now a typed Sampling object instead of a raw JsonObject. Recompilation is required; consumers reading sampling as JsonObject must migrate to the typed accessors.

watchosX64 and tvosX64 Kotlin/Native targets removed by @​devcrocod in #727

These targets have been deprecated upstream. Migrate to the corresponding ARM/simulator targets (watchosArm64, watchosSimulatorArm64, tvosArm64, tvosSimulatorArm64).

Features

  • Add $schema field to ToolSchema, declaring JSON Schema 2020-12 as the default dialect (SEP-1613) by @​devcrocod in #696
  • Validate tool names at registration time on the server, following the MCP tool naming standard (SEP-986) by @​devcrocod in #695
  • Add extensions field to ClientCapabilities and ServerCapabilities for advertising supported MCP extensions during the initialize handshake by @​eritscher in #678

Fixed

  • Validate the mcp-protocol-version HTTP header on initialization requests and correct DEFAULT_NEGOTIATED_PROTOCOL_VERSION to match the spec by @​devcrocod in #697
  • Propagate CancellationException instead of logging it as an error by @​devcrocod in #706
  • Evict stale GET SSE stream mapping on reconnect so a new GET on the same session is no longer silently rejected by the leftover entry from the previous stream by @​amr in #716

Maintenance

  • Add kotlinlang MCP server sample with search and get_page tools by @​devcrocod in #713
  • Migrate detekt to a buildSrc convention plugin with a fully explicit, version-locked YAML config by @​devcrocod in #729
  • Add Streamable HTTP integration tests for pagination, bad-request handling, and logging by @​jskjw157 in #613
  • Remove unused OpenAPI Generator plugin and configuration by @​devcrocod in #683
  • Remove Codecov configuration in favor of kover by @​devcrocod in #691
  • Remove redundant JUnit XML report configuration by @​devcrocod in #728
  • Remove IDEA files tied to the incompatible detekt IDE plugin by @​devcrocod in #714
  • Add Node.js setup to the release workflow so TypeScript integration tests run on macOS runners by @​devcrocod in #684

Dependencies

  • Kotlin to v2.3.21 in #704, #723
  • kotlinx.serialization-json to v1.11.0 in #687

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.1...0.12.0

Commits
  • c339d8c release: bump version to 0.12.0 (#731)
  • 64804a0 refactor: explicit locked detekt config (#729)
  • a238fc0 fix(server): evict stale GET SSE stream mapping on reconnect (#716)
  • 66d9efb test(streamable-http): add missing integration tests for pagination, bad requ...
  • f434fca feat!: SEP-1577 sampling with tools (#718)
  • 7474a33 chore: drop deprecated watchosX64 and tvosX64 targets (#727)
  • 12ace76 chore: remove redundant JUnit XML report configuration (#728)
  • ec606db chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-serv...
  • 1d93042 chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-server ...
  • 9817b17 chore(deps): bump org.jetbrains.kotlin.jvm from 2.2.21 to 2.3.21 in /samples/...
  • Additional commits viewable in compare view

Updates `io.modelcontextprotocol:kotlin-sdk-testing` from 0.11.1 to 0.12.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-testing's releases.

0.12.0

Description

Adds sampling-with-tools per SEP-1577, JSON Schema dialect declaration on tool schemas, server-side tool name validation, and capability extensions; fixes SSE reconnect on the same session and stops swallowing CancellationException.

Breaking Changes

Sampling messages can carry tool calls and results (SEP-1577) by @​devcrocod in #718

SamplingMessage.content is now typed as a new SamplingMessageContent supertype, with MediaContent (text/image/audio), ToolUseContent, and ToolResultContent as variants. MediaContent still exists and now extends SamplingMessageContent, so most source code that constructs sampling messages compiles unchanged — but the binary signatures of SamplingMessage, CreateMessageResult, and related types have changed. ClientCapabilities.sampling is also now a typed Sampling object instead of a raw JsonObject. Recompilation is required; consumers reading sampling as JsonObject must migrate to the typed accessors.

watchosX64 and tvosX64 Kotlin/Native targets removed by @​devcrocod in #727

These targets have been deprecated upstream. Migrate to the corresponding ARM/simulator targets (watchosArm64, watchosSimulatorArm64, tvosArm64, tvosSimulatorArm64).

Features

  • Add $schema field to ToolSchema, declaring JSON Schema 2020-12 as the default dialect (SEP-1613) by @​devcrocod in #696
  • Validate tool names at registration time on the server, following the MCP tool naming standard (SEP-986) by @​devcrocod in #695
  • Add extensions field to ClientCapabilities and ServerCapabilities for advertising supported MCP extensions during the initialize handshake by @​eritscher in #678

Fixed

  • Validate the mcp-protocol-version HTTP header on initialization requests and correct DEFAULT_NEGOTIATED_PROTOCOL_VERSION to match the spec by @​devcrocod in #697
  • Propagate CancellationException instead of logging it as an error by @​devcrocod in #706
  • Evict stale GET SSE stream mapping on reconnect so a new GET on the same session is no longer silently rejected by the leftover entry from the previous stream by @​amr in #716

Maintenance

  • Add kotlinlang MCP server sample with search and get_page tools by @​devcrocod in #713
  • Migrate detekt to a buildSrc convention plugin with a fully explicit, version-locked YAML config by @​devcrocod in #729
  • Add Streamable HTTP integration tests for pagination, bad-request handling, and logging by @​jskjw157 in #613
  • Remove unused OpenAPI Generator plugin and configuration by @​devcrocod in #683
  • Remove Codecov configuration in favor of kover by @​devcrocod in #691
  • Remove redundant JUnit XML report configuration by @​devcrocod in #728
  • Remove IDEA files tied to the incompatible detekt IDE plugin by @​devcrocod in #714
  • Add Node.js setup to the release workflow so TypeScript integration tests run on macOS runners by @​devcrocod in #684

Dependencies

  • Kotlin to v2.3.21 in #704, #723
  • kotlinx.serialization-json to v1.11.0 in #687

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.1...0.12.0

Commits
  • c339d8c release: bump version to 0.12.0 (#731)
  • 64804a0 refactor: explicit locked detekt config (#729)
  • a238fc0 fix(server): evict stale GET SSE stream mapping on reconnect (#716)
  • 66d9efb test(streamable-http): add missing integration tests for pagination, bad requ...
  • f434fca feat!: SEP-1577 sampling with tools (#718)
  • 7474a33 chore: drop deprecated watchosX64 and tvosX64 targets (#727)
  • 12ace76 chore: remove redundant JUnit XML report configuration (#728)
  • ec606db chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-serv...
  • 1d93042 chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-server ...
  • 9817b17 chore(deps): bump org.jetbrains.kotlin.jvm from 2.2.21 to 2.3.21 in /samples/...
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlinlang-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml index 2150c1070..723deb575 100644 --- a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.3.21" ktor = "3.3.3" -mcp-kotlin = "0.11.1" +mcp-kotlin = "0.12.0" serialization = "1.11.0" coroutines = "1.10.2" logback = "1.5.32" From bf2e339908ddc0f5494e1cb8313097b9f54b2313 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 10:16:40 +0200 Subject: [PATCH 129/187] chore(deps): bump mcp-kotlin from 0.11.1 to 0.12.0 in /samples/simple-streamable-server (#749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.11.1 to 0.12.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.11.1 to 0.12.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

0.12.0

Description

Adds sampling-with-tools per SEP-1577, JSON Schema dialect declaration on tool schemas, server-side tool name validation, and capability extensions; fixes SSE reconnect on the same session and stops swallowing CancellationException.

Breaking Changes

Sampling messages can carry tool calls and results (SEP-1577) by @​devcrocod in #718

SamplingMessage.content is now typed as a new SamplingMessageContent supertype, with MediaContent (text/image/audio), ToolUseContent, and ToolResultContent as variants. MediaContent still exists and now extends SamplingMessageContent, so most source code that constructs sampling messages compiles unchanged — but the binary signatures of SamplingMessage, CreateMessageResult, and related types have changed. ClientCapabilities.sampling is also now a typed Sampling object instead of a raw JsonObject. Recompilation is required; consumers reading sampling as JsonObject must migrate to the typed accessors.

watchosX64 and tvosX64 Kotlin/Native targets removed by @​devcrocod in #727

These targets have been deprecated upstream. Migrate to the corresponding ARM/simulator targets (watchosArm64, watchosSimulatorArm64, tvosArm64, tvosSimulatorArm64).

Features

  • Add $schema field to ToolSchema, declaring JSON Schema 2020-12 as the default dialect (SEP-1613) by @​devcrocod in #696
  • Validate tool names at registration time on the server, following the MCP tool naming standard (SEP-986) by @​devcrocod in #695
  • Add extensions field to ClientCapabilities and ServerCapabilities for advertising supported MCP extensions during the initialize handshake by @​eritscher in #678

Fixed

  • Validate the mcp-protocol-version HTTP header on initialization requests and correct DEFAULT_NEGOTIATED_PROTOCOL_VERSION to match the spec by @​devcrocod in #697
  • Propagate CancellationException instead of logging it as an error by @​devcrocod in #706
  • Evict stale GET SSE stream mapping on reconnect so a new GET on the same session is no longer silently rejected by the leftover entry from the previous stream by @​amr in #716

Maintenance

  • Add kotlinlang MCP server sample with search and get_page tools by @​devcrocod in #713
  • Migrate detekt to a buildSrc convention plugin with a fully explicit, version-locked YAML config by @​devcrocod in #729
  • Add Streamable HTTP integration tests for pagination, bad-request handling, and logging by @​jskjw157 in #613
  • Remove unused OpenAPI Generator plugin and configuration by @​devcrocod in #683
  • Remove Codecov configuration in favor of kover by @​devcrocod in #691
  • Remove redundant JUnit XML report configuration by @​devcrocod in #728
  • Remove IDEA files tied to the incompatible detekt IDE plugin by @​devcrocod in #714
  • Add Node.js setup to the release workflow so TypeScript integration tests run on macOS runners by @​devcrocod in #684

Dependencies

  • Kotlin to v2.3.21 in #704, #723
  • kotlinx.serialization-json to v1.11.0 in #687

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.1...0.12.0

Commits
  • c339d8c release: bump version to 0.12.0 (#731)
  • 64804a0 refactor: explicit locked detekt config (#729)
  • a238fc0 fix(server): evict stale GET SSE stream mapping on reconnect (#716)
  • 66d9efb test(streamable-http): add missing integration tests for pagination, bad requ...
  • f434fca feat!: SEP-1577 sampling with tools (#718)
  • 7474a33 chore: drop deprecated watchosX64 and tvosX64 targets (#727)
  • 12ace76 chore: remove redundant JUnit XML report configuration (#728)
  • ec606db chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-serv...
  • 1d93042 chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-server ...
  • 9817b17 chore(deps): bump org.jetbrains.kotlin.jvm from 2.2.21 to 2.3.21 in /samples/...
  • Additional commits viewable in compare view

Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.11.1 to 0.12.0
Release notes

Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

0.12.0

Description

Adds sampling-with-tools per SEP-1577, JSON Schema dialect declaration on tool schemas, server-side tool name validation, and capability extensions; fixes SSE reconnect on the same session and stops swallowing CancellationException.

Breaking Changes

Sampling messages can carry tool calls and results (SEP-1577) by @​devcrocod in #718

SamplingMessage.content is now typed as a new SamplingMessageContent supertype, with MediaContent (text/image/audio), ToolUseContent, and ToolResultContent as variants. MediaContent still exists and now extends SamplingMessageContent, so most source code that constructs sampling messages compiles unchanged — but the binary signatures of SamplingMessage, CreateMessageResult, and related types have changed. ClientCapabilities.sampling is also now a typed Sampling object instead of a raw JsonObject. Recompilation is required; consumers reading sampling as JsonObject must migrate to the typed accessors.

watchosX64 and tvosX64 Kotlin/Native targets removed by @​devcrocod in #727

These targets have been deprecated upstream. Migrate to the corresponding ARM/simulator targets (watchosArm64, watchosSimulatorArm64, tvosArm64, tvosSimulatorArm64).

Features

  • Add $schema field to ToolSchema, declaring JSON Schema 2020-12 as the default dialect (SEP-1613) by @​devcrocod in #696
  • Validate tool names at registration time on the server, following the MCP tool naming standard (SEP-986) by @​devcrocod in #695
  • Add extensions field to ClientCapabilities and ServerCapabilities for advertising supported MCP extensions during the initialize handshake by @​eritscher in #678

Fixed

  • Validate the mcp-protocol-version HTTP header on initialization requests and correct DEFAULT_NEGOTIATED_PROTOCOL_VERSION to match the spec by @​devcrocod in #697
  • Propagate CancellationException instead of logging it as an error by @​devcrocod in #706
  • Evict stale GET SSE stream mapping on reconnect so a new GET on the same session is no longer silently rejected by the leftover entry from the previous stream by @​amr in #716

Maintenance

  • Add kotlinlang MCP server sample with search and get_page tools by @​devcrocod in #713
  • Migrate detekt to a buildSrc convention plugin with a fully explicit, version-locked YAML config by @​devcrocod in #729
  • Add Streamable HTTP integration tests for pagination, bad-request handling, and logging by @​jskjw157 in #613
  • Remove unused OpenAPI Generator plugin and configuration by @​devcrocod in #683
  • Remove Codecov configuration in favor of kover by @​devcrocod in #691
  • Remove redundant JUnit XML report configuration by @​devcrocod in #728
  • Remove IDEA files tied to the incompatible detekt IDE plugin by @​devcrocod in #714
  • Add Node.js setup to the release workflow so TypeScript integration tests run on macOS runners by @​devcrocod in #684

Dependencies

  • Kotlin to v2.3.21 in #704, #723
  • kotlinx.serialization-json to v1.11.0 in #687

New Contributors

Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.11.1...0.12.0

Commits
  • c339d8c release: bump version to 0.12.0 (#731)
  • 64804a0 refactor: explicit locked detekt config (#729)
  • a238fc0 fix(server): evict stale GET SSE stream mapping on reconnect (#716)
  • 66d9efb test(streamable-http): add missing integration tests for pagination, bad requ...
  • f434fca feat!: SEP-1577 sampling with tools (#718)
  • 7474a33 chore: drop deprecated watchosX64 and tvosX64 targets (#727)
  • 12ace76 chore: remove redundant JUnit XML report configuration (#728)
  • ec606db chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-serv...
  • 1d93042 chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/kotlin-mcp-server ...
  • 9817b17 chore(deps): bump org.jetbrains.kotlin.jvm from 2.2.21 to 2.3.21 in /samples/...
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/simple-streamable-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index dc668c0d1..3296f053d 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin = "2.3.21" ktor = "3.3.3" junit = "6.0.3" -mcp-kotlin = "0.11.1" +mcp-kotlin = "0.12.0" slf4j = "2.0.17" shadow = "9.4.1" From 8a814292cff713cecb7339ef4a4c8444d6982fb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 08:39:40 +0000 Subject: [PATCH 130/187] chore(deps): bump zod from 4.3.6 to 4.4.3 in /integration-test/src/jvmTest/typescript in the all-dependencies group across 1 directory (#751) Bumps the all-dependencies group with 1 update in the /integration-test/src/jvmTest/typescript directory: [zod](https://github.com/colinhacks/zod). Updates `zod` from 4.3.6 to 4.4.3
Release notes

Sourced from zod's releases.

v4.4.3

Commits:

  • 4c2fa95ce3f3390fbc522324e406b4e9e89b88f9 docs: use Zernio primary wordmark for gold sponsor logo
  • 2aeec83eb135e3a83756e973ef44845fc5a455d2 docs: prune lapsed gold sponsors and rebalance logo sizing
  • 7391be88ac1ee5cd02057f5ccc012a1f5df4efd0 docs: prune lapsed silver/bronze sponsors and add active ones
  • 2c703322a21b4e2b12f33f49ea8430c451a68b4f docs: normalize bronze sponsor logos to github avatar pattern
  • 9195250cab0e7950efe39c3926d6c203b4b0a170 docs: remove Mintlify from bronze sponsors (churned)
  • b8dffe9e62f17e6571e6249d05cc5102b54d94e4 docs: remove Numeric and Speakeasy (2+ missed monthly cycles)
  • 1cab69383fcdeae2a366d5e2a2fc4d8fc765d168 fix(v4): restore catch handling for absent object keys (#5937) (#5939)
  • c2be4f819064eed62c7c350a2d399b5faecd15f8 fix(v4): generalize optin/fallback to transform; restore preprocess on absent keys (#5941)
  • f3c9ec03ba7a28ae72d25cc295f38674bee0f559 4.4.3
  • 1fb56a5c18c27102dbc92260a4007c7732a0ccca docs: document release procedure in AGENTS.md

v4.4.2

Commits:

  • 0c62df0ea19fd05abdf90473e9eef7eea530fab2 Clean up docs navigation and stale labels (#5901)
  • 20cc794895cc8604fe0c87d83a5d1c3f89fad0ac chore: add security policy and refresh tooling deps
  • 6fbe07b0177efdd1bf1c0b05160e70d7a0702337 fix(docs): heading anchor links now include the hash so it doesnt scoll all the way up, follows navbar logic (#5791)
  • 4bbed1b1c73eca4ce9e59b1189ed236aa6c8b5bd Tighten discriminated union option typing
  • bbac3e567e7fccfaaf7cdc97f1ce30c295e2c908 Update PR guidance for agents
  • cf0dc942a32805c292fff59ade20a7ace980735a Merge remote-tracking branch 'origin/main' into fix-discriminated-union-key-constraint
  • 292c894a5fd2aa42e527900b83d8d7a3009a709c docs: add Zernio gold sponsor
  • 1fc9f311c28dcf80d0bb5a36b177086cbc3d8eca docs: document codec inversion
  • 1373c85da9aeff704a9762d27bc58699618aefb7 docs: remove AI disclosure guidance
  • e20d02b473c08e3a4e557bc610b1b5fac079b649 chore: ignore triage notes
  • e58ea4d91b1dfe8194b73508203213cbc7e9c936 docs: test Zod Mini tab code heights
  • 905761a5d127e8d5dd2ebb3bc88c75cb0b8149ff docs: document preprocess input type narrowing
  • bf64bac850d4dee2b7dde7e64909d5d796d32043 chore: tighten test guidance in AGENTS.md
  • 8ec4e73f4c4693b6361ad591be40fb41eb8a9f95 chore: update play.ts scratch
  • 02c2baf7d0d615872fa4528a8020603b71211702 Make z.preprocess defer optionality to inner schema (#5929)
  • 88015df8e25c44fb5385eb3ef28935119cd5edea fix(docs): drop deprecated baseUrl from tsconfig
  • c59d4474e3b4cad1b323462186cf607178ce8267 4.4.2

v4.4.1

Commits:

  • 481f7be4238c83ed58183f921b2646f340a91c6a ci: gate release publishing on full test workflow
  • 95ccab423aec720b2523c3a64cdc7e3204537cc7 test(v3): restore optional undefined expectations
  • cede2c63739a5823d6aa5093d291e9a111da943d fix(v4): reject tuple holes before required defaults (#5900)
  • edd0bf0f5ada4a8dc581c259407d7bbad0a71ea7 release: 4.4.1
  • 180d83d1dbe6a59260710cc8637a3dea2281ee56 docs: remove Jazz featured sponsor

v4.4.0

4.4.0

This is a minor release with a wide set of correctness and soundness fixes. Some fixes intentionally make Zod stricter, so code that depended on previously accepted invalid or ambiguous inputs may need small updates.

Potentially breaking bug fixes

... (truncated)

Commits
  • 1fb56a5 docs: document release procedure in AGENTS.md
  • f3c9ec0 4.4.3
  • c2be4f8 fix(v4): generalize optin/fallback to transform; restore preprocess on absent...
  • 1cab693 fix(v4): restore catch handling for absent object keys (#5937) (#5939)
  • b8dffe9 docs: remove Numeric and Speakeasy (2+ missed monthly cycles)
  • 9195250 docs: remove Mintlify from bronze sponsors (churned)
  • 2c70332 docs: normalize bronze sponsor logos to github avatar pattern
  • 7391be8 docs: prune lapsed silver/bronze sponsors and add active ones
  • 2aeec83 docs: prune lapsed gold sponsors and rebalance logo sizing
  • 4c2fa95 docs: use Zernio primary wordmark for gold sponsor logo
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for zod since your current version.


Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 8 ++++---- integration-test/src/jvmTest/typescript/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 34e6bf45e..0d258edff 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "express": "^5.2.1", - "zod": "^4.3.6" + "zod": "^4.4.3" }, "devDependencies": { "@types/cors": "^2.8.19", @@ -1797,9 +1797,9 @@ "license": "ISC" }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index 030c79ca1..d648fa9fd 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -11,7 +11,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "express": "^5.2.1", - "zod": "^4.3.6" + "zod": "^4.4.3" }, "devDependencies": { "@types/cors": "^2.8.19", From 7b474720f46c9e9d416140601960e35d66254815 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 13:49:33 +0200 Subject: [PATCH 131/187] chore(deps): bump hono from 4.12.14 to 4.12.18 in /integration-test/src/jvmTest/typescript (#755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [hono](https://github.com/honojs/hono) from 4.12.14 to 4.12.18.
Release notes

Sourced from hono's releases.

v4.12.18

Security fixes

This release includes fixes for the following security issues:

Cache Middleware ignores Vary: Authorization / Vary: Cookie leading to cross-user cache leakage

Affects: Cache Middleware. Fixes missing cache-skip handling for Vary: Authorization and Vary: Cookie, where a response cached for one authenticated user could be served to other users. GHSA-p77w-8qqv-26rm

CSS Declaration Injection via Style Object Values in JSX SSR

Affects: hono/jsx. Fixes a missing CSS-context escape for style object values and property names, where untrusted input could inject additional CSS declarations. The impact is limited to CSS and does not allow JavaScript execution. GHSA-qp7p-654g-cw7p

Improper validation of NumericDate claims (exp, nbf, iat) in JWT verify()

Affects: hono/utils/jwt. Fixes improper validation of exp, nbf, and iat claims, where falsy, non-finite, or non-numeric values could silently bypass time-based checks instead of being rejected per RFC 7519. GHSA-hm8q-7f3q-5f36


Users who use the JWT helper, hono/jsx, or the Cache middleware are strongly encouraged to upgrade to this version.

v4.12.17

What's Changed

New Contributors

Full Changelog: https://github.com/honojs/hono/compare/v4.12.16...v4.12.17

v4.12.16

Security fixes

This release includes fixes for the following security issues:

Unvalidated JSX Tag Names in hono/jsx May Allow HTML Injection

Affects: hono/jsx. Fixes missing validation of JSX tag names when using jsx() or createElement(), which could allow HTML injection if untrusted input is used as the tag name. GHSA-69xw-7hcm-h432

bodyLimit() can be bypassed for chunked / unknown-length requests

Affects: Body Limit Middleware. Fixes late enforcement for request bodies without a reliable Content-Length (e.g. chunked requests), where oversized requests could reach handlers and return successful responses before being rejected. GHSA-9vqf-7f2p-gf9v

v4.12.15

What's Changed

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=hono&package-manager=npm_and_yarn&previous-version=4.12.14&new-version=4.12.18)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/modelcontextprotocol/kotlin-sdk/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 0d258edff..b71ef4c4c 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -1197,9 +1197,9 @@ } }, "node_modules/hono": { - "version": "4.12.14", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", - "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", "license": "MIT", "engines": { "node": ">=16.9.0" From bab4b0bc606bc193a863b2c428e247e963cc9f8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 12:11:20 +0000 Subject: [PATCH 132/187] chore(deps): bump @types/node from 25.6.0 to 25.6.2 in /integration-test/src/jvmTest/typescript in the all-dependencies group across 1 directory (#762) Bumps the all-dependencies group with 1 update in the /integration-test/src/jvmTest/typescript directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 25.6.0 to 25.6.2
Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 8 ++++---- integration-test/src/jvmTest/typescript/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index b71ef4c4c..d66c9ee2f 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -15,7 +15,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.6.0", + "@types/node": "^25.6.2", "cors": "^2.8.6", "tsx": "^4.21.0", "typescript": "^6.0.3" @@ -579,9 +579,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index d648fa9fd..6ec358a83 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.6.0", + "@types/node": "^25.6.2", "cors": "^2.8.6", "tsx": "^4.21.0", "typescript": "^6.0.3" From 6d34e6ce03b87b95570ce67f666c942a5107fcf2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 12:48:46 +0000 Subject: [PATCH 133/187] chore(deps): bump fast-uri from 3.1.0 to 3.1.2 in /integration-test/src/jvmTest/typescript (#756) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2.
Release notes

Sourced from fast-uri's releases.

v3.1.2

⚠️ Security Release

What's Changed

Full Changelog: https://github.com/fastify/fast-uri/compare/v3.1.1...v3.1.2

v3.1.1

⚠️ Security Release

What's Changed

New Contributors

Full Changelog: https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.1

Commits
  • 919dd8e Bumped v3.1.2
  • c65ba57 fixup: linting
  • 6c86c17 Merge commit from fork
  • a95158a Handle malformed fragment decoding without throwing (#171)
  • cea547c Bumped v3.1.1
  • 876ce79 Merge commit from fork
  • dcdf690 ci: add lock-threads workflow (#169)
  • c860e65 build(deps-dev): bump neostandard from 0.12.2 to 0.13.0 (#167)
  • 9b4c6dc build(deps): bump fastify/workflows/.github/workflows/plugins-ci.yml (#166)
  • 85d09a9 build(deps): bump fastify/workflows/.github/workflows/plugins-ci-package-mana...
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index d66c9ee2f..6aa468d7f 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -1033,9 +1033,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", From 664c0da900297cef8589a418d6517c86b9a51353 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 18:44:47 +0200 Subject: [PATCH 134/187] chore(deps): bump com.anthropic:anthropic-java from 2.28.0 to 2.30.0 in /samples/kotlin-mcp-client in the other-dependencies group across 1 directory (#757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-client directory: [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java). Updates `com.anthropic:anthropic-java` from 2.28.0 to 2.30.0
Release notes

Sourced from com.anthropic:anthropic-java's releases.

v2.30.0

2.30.0 (2026-05-06)

Full Changelog: v2.29.0...v2.30.0

Features

  • api: add support for Managed Agents multiagents and outcomes, webhooks, vault validation (e641d5f)
  • api: add support for Managed Agents multiagents and outcomes, webhooks, vault validation (6067599)
  • client: support proxy authentication (d91fea2)

Bug Fixes

  • api: Adjust webhook configuration (34ea5b4)

Chores

v2.29.0

2.29.0 (2026-05-05)

Full Changelog: v2.28.0...v2.29.0

Features

  • client: allow targeting a workspace for OIDC federation token exchange (578003a)

Performance Improvements

  • client: create one json mapper (556ef49)

Chores

  • remove duplicated dokka setup (d6b94f4)

Documentation

Changelog

Sourced from com.anthropic:anthropic-java's changelog.

2.30.0 (2026-05-06)

Full Changelog: v2.29.0...v2.30.0

Features

  • api: add support for Managed Agents multiagents and outcomes, webhooks, vault validation (e641d5f)
  • api: add support for Managed Agents multiagents and outcomes, webhooks, vault validation (6067599)
  • client: support proxy authentication (d91fea2)

Bug Fixes

  • api: Adjust webhook configuration (34ea5b4)

Chores

2.29.0 (2026-05-05)

Full Changelog: v2.28.0...v2.29.0

Features

  • client: allow targeting a workspace for OIDC federation token exchange (578003a)

Performance Improvements

  • client: create one json mapper (556ef49)

Chores

  • remove duplicated dokka setup (d6b94f4)

Documentation

Commits
  • 5f413b5 chore: release main
  • 46baee8 fix(api): Adjust webhook configuration
  • 0ce73ea feat(api): add support for Managed Agents multiagents and outcomes, webhooks,...
  • 337eb3b chore: fix merge errors
  • 0a95f16 feat(api): add support for Managed Agents multiagents and outcomes, webhooks,...
  • 93912dd feat(client): support proxy authentication
  • 1fc9ab1 chore: release main
  • 9e4cbbd feat(client): allow targeting a workspace for OIDC federation token exchange
  • e8e5894 perf(client): create one json mapper
  • 726e6f4 chore: remove duplicated dokka setup
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.anthropic:anthropic-java&package-manager=gradle&previous-version=2.28.0&new-version=2.30.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 2959a9a1a..6d4a97930 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -anthropic = "2.28.0" +anthropic = "2.31.0" kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.12.0" From 9947353a48e13b3871d5b9ca1c87c7e15a0e92c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 17:04:01 +0000 Subject: [PATCH 135/187] chore(deps): bump coroutines from 1.10.2 to 1.11.0 in /samples/kotlinlang-mcp-server (#758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `coroutines` from 1.10.2 to 1.11.0. Updates `org.jetbrains.kotlinx:kotlinx-coroutines-core` from 1.10.2 to 1.11.0
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-core's releases.

1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. B efore, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

... (truncated)

Changelog

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-core's changelog.

Version 1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. Before, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

Version 1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

Version 1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

... (truncated)

Commits
  • 8564f65 Version 1.11.0
  • a4c6af9 Merge remote-tracking branch 'origin/master' into develop
  • ef917b4 KT-84955: mark apple x64 tagets as deprecated error (#4645)
  • 5ebc421 Update the release procedure description (#4670)
  • 95f46a0 Remove old maven repository settings (#4672)
  • b4f4f0a Fix package name of ToMapCollectionSamplesTest. (#4674)
  • 86738dc Added templates to the issue creation wizard (#4654)
  • 330fcc2 Version 1.11.0-rc02
  • e31cef6 Merge remote-tracking branch 'origin/master' into develop
  • dc6e9f6 Restore Promise-related functions on Wasm/JS as HIDDEN (#4661)
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlinx:kotlinx-coroutines-test` from 1.10.2 to 1.11.0
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-test's releases.

1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. B efore, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

... (truncated)

Changelog

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-test's changelog.

Version 1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. Before, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

Version 1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

Version 1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

... (truncated)

Commits
  • 8564f65 Version 1.11.0
  • a4c6af9 Merge remote-tracking branch 'origin/master' into develop
  • ef917b4 KT-84955: mark apple x64 tagets as deprecated error (#4645)
  • 5ebc421 Update the release procedure description (#4670)
  • 95f46a0 Remove old maven repository settings (#4672)
  • b4f4f0a Fix package name of ToMapCollectionSamplesTest. (#4674)
  • 86738dc Added templates to the issue creation wizard (#4654)
  • 330fcc2 Version 1.11.0-rc02
  • e31cef6 Merge remote-tracking branch 'origin/master' into develop
  • dc6e9f6 Restore Promise-related functions on Wasm/JS as HIDDEN (#4661)
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlinlang-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml index 723deb575..5b7988e24 100644 --- a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.12.0" serialization = "1.11.0" -coroutines = "1.10.2" +coroutines = "1.11.0" logback = "1.5.32" shadow = "9.4.1" From a967df1eac5b69df7d4df522612255f38073f69b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 19:29:28 +0200 Subject: [PATCH 136/187] chore(deps): bump org.jetbrains.kotlinx:kotlinx-coroutines-test from 1.10.2 to 1.11.0 in /samples/weather-stdio-server (#759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [org.jetbrains.kotlinx:kotlinx-coroutines-test](https://github.com/Kotlin/kotlinx.coroutines) from 1.10.2 to 1.11.0.
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-test's releases.

1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. B efore, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

... (truncated)

Changelog

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-test's changelog.

Version 1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. Before, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

Version 1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

Version 1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

... (truncated)

Commits
  • 8564f65 Version 1.11.0
  • a4c6af9 Merge remote-tracking branch 'origin/master' into develop
  • ef917b4 KT-84955: mark apple x64 tagets as deprecated error (#4645)
  • 5ebc421 Update the release procedure description (#4670)
  • 95f46a0 Remove old maven repository settings (#4672)
  • b4f4f0a Fix package name of ToMapCollectionSamplesTest. (#4674)
  • 86738dc Added templates to the issue creation wizard (#4654)
  • 330fcc2 Version 1.11.0-rc02
  • e31cef6 Merge remote-tracking branch 'origin/master' into develop
  • dc6e9f6 Restore Promise-related functions on Wasm/JS as HIDDEN (#4661)
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index 1fc2e6a29..8eee2982e 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -coroutines = "1.10.2" +coroutines = "1.11.0" kotlin = "2.3.21" ktor = "3.3.3" logging = "8.0.02" From d6027fd46d8b17cc4b5587041d5a2128b2f528bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 20:09:56 +0200 Subject: [PATCH 137/187] chore(deps): bump the kotlinx group across 1 directory with 3 updates (#760) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the kotlinx group with 3 updates in the / directory: [org.jetbrains.kotlinx:kotlinx-coroutines-core](https://github.com/Kotlin/kotlinx.coroutines), [org.jetbrains.kotlinx:kotlinx-coroutines-core-wasm-js](https://github.com/Kotlin/kotlinx.coroutines) and [org.jetbrains.kotlinx:kotlinx-coroutines-test](https://github.com/Kotlin/kotlinx.coroutines). Updates `org.jetbrains.kotlinx:kotlinx-coroutines-core` from 1.10.2 to 1.11.0
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-core's releases.

1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. B efore, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

... (truncated)

Changelog

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-core's changelog.

Version 1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. Before, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

Version 1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

Version 1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

... (truncated)

Commits
  • 8564f65 Version 1.11.0
  • a4c6af9 Merge remote-tracking branch 'origin/master' into develop
  • ef917b4 KT-84955: mark apple x64 tagets as deprecated error (#4645)
  • 5ebc421 Update the release procedure description (#4670)
  • 95f46a0 Remove old maven repository settings (#4672)
  • b4f4f0a Fix package name of ToMapCollectionSamplesTest. (#4674)
  • 86738dc Added templates to the issue creation wizard (#4654)
  • 330fcc2 Version 1.11.0-rc02
  • e31cef6 Merge remote-tracking branch 'origin/master' into develop
  • dc6e9f6 Restore Promise-related functions on Wasm/JS as HIDDEN (#4661)
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlinx:kotlinx-coroutines-core-wasm-js` from 1.10.2 to 1.11.0
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-core-wasm-js's releases.

1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. B efore, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

... (truncated)

Changelog

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-core-wasm-js's changelog.

Version 1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. Before, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

Version 1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

Version 1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

... (truncated)

Commits
  • 8564f65 Version 1.11.0
  • a4c6af9 Merge remote-tracking branch 'origin/master' into develop
  • ef917b4 KT-84955: mark apple x64 tagets as deprecated error (#4645)
  • 5ebc421 Update the release procedure description (#4670)
  • 95f46a0 Remove old maven repository settings (#4672)
  • b4f4f0a Fix package name of ToMapCollectionSamplesTest. (#4674)
  • 86738dc Added templates to the issue creation wizard (#4654)
  • 330fcc2 Version 1.11.0-rc02
  • e31cef6 Merge remote-tracking branch 'origin/master' into develop
  • dc6e9f6 Restore Promise-related functions on Wasm/JS as HIDDEN (#4661)
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlinx:kotlinx-coroutines-test` from 1.10.2 to 1.11.0
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-test's releases.

1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. B efore, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

... (truncated)

Changelog

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-test's changelog.

Version 1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. Before, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

Version 1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

Version 1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

... (truncated)

Commits
  • 8564f65 Version 1.11.0
  • a4c6af9 Merge remote-tracking branch 'origin/master' into develop
  • ef917b4 KT-84955: mark apple x64 tagets as deprecated error (#4645)
  • 5ebc421 Update the release procedure description (#4670)
  • 95f46a0 Remove old maven repository settings (#4672)
  • b4f4f0a Fix package name of ToMapCollectionSamplesTest. (#4674)
  • 86738dc Added templates to the issue creation wizard (#4654)
  • 330fcc2 Version 1.11.0-rc02
  • e31cef6 Merge remote-tracking branch 'origin/master' into develop
  • dc6e9f6 Restore Promise-related functions on Wasm/JS as HIDDEN (#4661)
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlinx:kotlinx-coroutines-core-wasm-js` from 1.10.2 to 1.11.0
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-core-wasm-js's releases.

1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. B efore, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

... (truncated)

Changelog

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-core-wasm-js's changelog.

Version 1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web target. On Wasm/JS, this is a breaking change. Before the change, Promise on Wasm/JS could work with arbitrary Kotlin types, but now, only JsAny subtypes are accepted (#4563).
  • Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. Before, exceptions were logged, but now, they are reported to the JS runtime (#4451, #4631).
  • Deprecated using CoroutineDispatcher as the coroutine context key; now, ContinuationInterceptor has to be used instead (#4333).
  • Advanced the deprecation levels on kotlinx-coroutines-test APIs (#4604).
  • Added lint functions that mark passing a Job to coroutine builders as deprecated (#4435).

Bug fixes and improvements

  • Added a callsInPlace(EXACTLY_ONCE) contract to runBlocking in code shared between JVM and Native (#4368).
  • Added a callsInPlace(EXACTLY_ONCE) contract to suspendCancellableCoroutine (#4574).
  • Fixed flowOn incorrectly handling ThreadContextElement updates (#4403).
  • Fixed exceptions in user-supplied Thread.UncaughtExceptionHandler instances causing the internal coroutines machinery to fail (#4516).
  • Fixed CoroutineDispatcher.asScheduler in the RxJava integration not cancelling outstanding work when a Worker gets cancelled, which led to memory leaks in some scenarios (#4615).
  • Fixed SharedFlow entering an invalid state when a subscriber and an emitter are cancelled simultaneously (#4583).
  • Fixed an R8 optimization leading to shareIn/stateIn coroutines getting garbage-collected (#4646). Thanks, @​solevic!

Small additions

  • Added CompletableDeferred.asDeferred for obtaining a read-only Deferred view (#4408).
  • Added SharedFlow.asFlow for obtaining a Flow view with hidden hot flow semantics (#4530). Thanks, @​g000sha256!
  • Added a StateFlow.collectLatest overload returning Nothing to assist with finding unreachable code (#4454).
  • Added ReceiveChannel.consumeTo for consuming a ReceiveChannel into a MutableCollection (#4520).
  • Added a StateFlow<T>.onSubscription overload returning a StateFlow<T>, similar to SharedFlow<T>.onSubscription returning SharedFlow<T> (#4275). Thanks, @​xit0c!
  • Added terminal Flow operators for collecting a Flow to a Map (#1541).

Changelog relative to version 1.11.0

No changes, only the version is increased.

Version 1.11.0-rc02

Restored binary compatibility with 1.10.2 and older versions on Wasm/JS for usages of Promise-related functions (#4661).

Version 1.11.0-rc01

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

... (truncated)

Commits
  • 8564f65 Version 1.11.0
  • a4c6af9 Merge remote-tracking branch 'origin/master' into develop
  • ef917b4 KT-84955: mark apple x64 tagets as deprecated error (#4645)
  • 5ebc421 Update the release procedure description (#4670)
  • 95f46a0 Remove old maven repository settings (#4672)
  • b4f4f0a Fix package name of ToMapCollectionSamplesTest. (#4674)
  • 86738dc Added templates to the issue creation wizard (#4654)
  • 330fcc2 Version 1.11.0-rc02
  • e31cef6 Merge remote-tracking branch 'origin/master' into develop
  • dc6e9f6 Restore Promise-related functions on Wasm/JS as HIDDEN (#4661)
  • Additional commits viewable in compare view

Updates `org.jetbrains.kotlinx:kotlinx-coroutines-test` from 1.10.2 to 1.11.0
Release notes

Sourced from org.jetbrains.kotlinx:kotlinx-coroutines-test's releases.

1.11.0

Various

  • Kotlin was updated to 2.2.20 (#4545).
  • Improved the published jar files (#3842, #4599).
  • Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc (#4433, #4596).

Breaking changes and deprecations

  • Moved Promise-related functions from JS and Wasm/JS to the new web 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 63a75c574..a1f2cc0f3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ netty = "4.2.13.Final" # libraries version awaitility = "4.3.0" collections-immutable = "0.4.0" -coroutines = "1.10.2" +coroutines = "1.11.0" kotest = "6.1.11" kotlinx-io = "0.9.0" ktor = "3.3.3" From 44e012e4eefc9ad1128a814c0be09769613367ba Mon Sep 17 00:00:00 2001 From: johny <69754601+jskjw157@users.noreply.github.com> Date: Sat, 16 May 2026 01:44:16 +0900 Subject: [PATCH 138/187] fix(client): invoke onClose when SSE session disconnects (#437) (#738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the SSE client transport so that registered `onClose` callbacks fire when the underlying SSE session ends — whether the server closes the stream gracefully or an error event terminates it. closes #437 ## Motivation and Context `SseClientTransport.collectMessages` already runs `closeResources()` in its `finally` block when `session.incoming` completes, but it never invoked the `onClose` callback chain. As a result, the MCP `Client` (and any user code calling `transport.onClose { ... }`) had no way to detect that the server-side SSE connection had dropped. The other client transports already do this correctly: - `StdioClientTransport` calls `invokeOnCloseCallback()` from its read-loop `finally` (kotlin-sdk-client/src/.../StdioClientTransport.kt:224). - The protected `AbstractTransport.invokeOnCloseCallback()` helper guards against duplicate firing with an `AtomicBoolean`, so it is safe to call from both the read-loop `finally` and the user-initiated `close()` path. The fix mirrors that established pattern in one line. ## How Has This Been Tested? Added `onClose callback fires when the SSE stream is disconnected by the server` to `SseClientTransportTest`. The test: 1. Starts the transport against a mock SSE engine. 2. Closes the SSE byte channel server-side via a new `MockSseClientEngine.disconnectSseStream()` helper. 3. Asserts the registered `onClose` callback fires (using Kotest `eventually`, since the close propagates on a real-time dispatcher). I also verified the test fails before the fix and passes after. ``` ./gradlew :kotlin-sdk-client:jvmTest ./gradlew apiCheck ktlintCheck detekt ``` ## Breaking Changes None. `invokeOnCloseCallback()` is `protected` on `AbstractTransport`; this PR only adds a call site inside `SseClientTransport`. No public API surface changes. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .../kotlin/sdk/client/SseClientTransport.kt | 1 + .../sdk/client/sse/MockSseClientEngine.kt | 8 ++++++ .../sdk/client/sse/SseClientTransportTest.kt | 27 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt index a48db608d..0295eb9a3 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt @@ -136,6 +136,7 @@ public class SseClientTransport( throw e } finally { closeResources() + invokeOnCloseCallback() } } diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt index 3627b6c3a..14af5461d 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt @@ -53,6 +53,14 @@ internal open class MockSseClientEngine( job.cancel() } + /** + * Closes the SSE byte channel without disposing the engine, simulating a server-side + * disconnect of the SSE stream. + */ + fun disconnectSseStream() { + channel.close() + } + private fun responseContext(): CoroutineContext = dispatcher + Job() private suspend fun respondWithSseEndpointEvent(data: HttpRequestData): HttpResponseData { diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt index 6cc7f6e93..319ba2a82 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt @@ -1,5 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.client.sse +import io.kotest.assertions.nondeterministic.eventually import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe import io.ktor.client.HttpClient @@ -9,6 +10,7 @@ import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification import kotlinx.coroutines.test.runTest import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds class SseClientTransportTest { @@ -90,6 +92,31 @@ class SseClientTransportTest { engine.close() } + @Test + fun `onClose callback fires when the SSE stream is disconnected by the server`() = runTest { + // Given + val sseUrl = "http://example.com/api/mcp/sse" + + // And + val engine = CapturingSseClientEngine(endpoint = "/messages?sessionId=abc") + val transport = sseTransport(sseUrl, engine) + var onCloseFired = false + transport.onClose { onCloseFired = true } + + // When + transport.start() + engine.disconnectSseStream() + + // Then + eventually(2.seconds) { + onCloseFired shouldBe true + } + + // Cleanup + transport.close() + engine.close() + } + private fun sseTransport(sseUrl: String, engine: CapturingSseClientEngine) = SseClientTransport(HttpClient(engine) { install(SSE) }, sseUrl) From 664f51b9cc210466c02699f5146130f86c4be98b Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 18 May 2026 12:17:26 +0200 Subject: [PATCH 139/187] test: remove mokksy dependency (#754) Drops`dev.mokksy:mokksy` dependency ## Motivation and Context `mokksy` is a niche, single-maintainer library with very limited external adoption, introduced by its own author. Keeping a test-only dependency on it is unnecessary supply-chain exposure given that the two patterns already used in this repo, `MockEngine` for transport-level tests and `embeddedServer` for integration-level tests, cover everything it provided. ## Breaking Changes none ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- gradle/libs.versions.toml | 2 - ...mableHttpClientTransportIntegrationTest.kt | 270 ++++++++++++++++ kotlin-sdk-client/build.gradle.kts | 1 - .../http/StreamableHttpClientTransportTest.kt | 73 +++++ .../AbstractStreamableHttpClientTest.kt | 42 --- .../kotlin/sdk/client/MockMcp.kt | 237 -------------- .../sdk/client/StreamableHttpClientTest.kt | 293 ------------------ 7 files changed, 343 insertions(+), 575 deletions(-) create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpClientTransportIntegrationTest.kt delete mode 100644 kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt delete mode 100644 kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt delete mode 100644 kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a1f2cc0f3..5b04ca45d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,6 @@ kotlinx-io = "0.9.0" ktor = "3.3.3" logging = "8.0.02" mockk = "1.14.9" -mokksy = "0.8.1" serialization = "1.11.0" slf4j = "2.0.17" logback = "1.5.32" @@ -65,7 +64,6 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" } ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } -mokksy = { group = "dev.mokksy", name = "mokksy", version.ref = "mokksy" } netty-bom = { group = "io.netty", name = "netty-bom", version.ref = "netty" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpClientTransportIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpClientTransportIntegrationTest.kt new file mode 100644 index 000000000..598825f13 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpClientTransportIntegrationTest.kt @@ -0,0 +1,270 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp + +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.shouldBe +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.server.engine.embeddedServer +import io.ktor.server.request.receiveText +import io.ktor.server.response.header +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.response.respondTextWriter +import io.ktor.server.routing.Route +import io.ktor.server.routing.delete +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.Tool +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import io.modelcontextprotocol.kotlin.test.utils.actualPort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject +import kotlin.test.Test +import io.ktor.client.engine.cio.CIO as ClientCIO +import io.ktor.server.cio.CIO as ServerCIO + +private const val SESSION_ID_HEADER = "mcp-session-id" +private const val HOST = "127.0.0.1" +private const val MCP_PATH = "/mcp" + +private val INITIALIZE_RESULT = """ + { + "capabilities": {"tools": {"listChanged": false}}, + "protocolVersion": "2025-03-26", + "serverInfo": {"name": "test-server", "version": "1.0.0"} + } +""".trimIndent() + +private val TOOLS_LIST_RESULT = """ + { + "tools": [ + { + "name": "get_weather", + "title": "Weather Information Provider", + "description": "Get current weather information for a location", + "inputSchema": { + "type": "object", + "properties": { + "location": {"type": "string", "description": "City name or zip code"} + }, + "required": ["location"] + }, + "outputSchema": { + "type": "object", + "properties": { + "temperature": {"type": "number", "description": "Temperature, Celsius"} + }, + "required": ["temperature"] + }, + "_meta": {} + } + ] + } +""".trimIndent() + +private const val PROGRESS_NOTIFICATION_1 = + """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""" + +private const val PROGRESS_NOTIFICATION_2 = + """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":75,"total":100}}""" + +internal class StreamableHttpClientTransportIntegrationTest { + + @Test + fun `client receives progress notifications via GET SSE and lists tools`(): Unit = runBlocking(Dispatchers.IO) { + val sessionId = "sid-progress" + runWithServer({ + mcpPostRoute(sessionId, extraResults = mapOf("tools/list" to TOOLS_LIST_RESULT)) + get(MCP_PATH) { + call.response.header(SESSION_ID_HEADER, sessionId) + call.respondTextWriter(contentType = ContentType.Text.EventStream) { + write(sseEvent(id = "1", data = PROGRESS_NOTIFICATION_1)) + flush() + delay(200) + write(sseEvent(id = "2", data = PROGRESS_NOTIFICATION_2)) + flush() + awaitCancellation() + } + } + }) { url -> + val client = newClient() + client.connect(StreamableHttpClientTransport(client = newHttpClient(), url = url)) + + val listToolsResult = client.listTools() + + listToolsResult.tools shouldContain Tool( + name = "get_weather", + title = "Weather Information Provider", + description = "Get current weather information for a location", + inputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("location") { + put("type", "string") + put("description", "City name or zip code") + } + }, + required = listOf("location"), + ), + outputSchema = ToolSchema( + properties = buildJsonObject { + putJsonObject("temperature") { + put("type", "number") + put("description", "Temperature, Celsius") + } + }, + required = listOf("temperature"), + ), + annotations = null, + meta = EmptyJsonObject, + ) + + client.close() + } + } + + @Test + fun `terminateSession sends DELETE request and clears sessionId`(): Unit = runBlocking(Dispatchers.IO) { + val sessionId = "sid-terminate" + runWithServer({ + mcpPostRoute(sessionId) + get(MCP_PATH) { + call.response.header(SESSION_ID_HEADER, sessionId) + call.respondTextWriter(contentType = ContentType.Text.EventStream) { + awaitCancellation() + } + } + delete(MCP_PATH) { + call.respond(HttpStatusCode.OK) + } + }) { url -> + val client = newClient() + val transport = StreamableHttpClientTransport(client = newHttpClient(), url = url) + client.connect(transport) + + transport.sessionId shouldBe sessionId + transport.terminateSession() + transport.sessionId shouldBe null + + client.close() + } + } + + @Test + fun `client survives 405 on GET SSE`(): Unit = runBlocking(Dispatchers.IO) { + val sessionId = "sid-405" + runWithServer({ + mcpPostRoute(sessionId, extraResults = mapOf("ping" to "{}")) + get(MCP_PATH) { + call.respondText( + text = "", + contentType = ContentType.Text.EventStream, + status = HttpStatusCode.MethodNotAllowed, + ) + } + }) { url -> + val client = newClient() + client.connect(StreamableHttpClientTransport(client = newHttpClient(), url = url)) + + client.ping() + + client.close() + } + } + + @Test + fun `client survives non streaming JSON response on GET SSE`(): Unit = runBlocking(Dispatchers.IO) { + val sessionId = "sid-json" + runWithServer({ + mcpPostRoute(sessionId, extraResults = mapOf("ping" to "{}")) + get(MCP_PATH) { + call.response.header(SESSION_ID_HEADER, sessionId) + call.respondText( + text = "", + contentType = ContentType.Application.Json, + status = HttpStatusCode.OK, + ) + } + }) { url -> + val client = newClient() + client.connect(StreamableHttpClientTransport(client = newHttpClient(), url = url)) + + client.ping() + + client.close() + } + } + + private fun Route.mcpPostRoute(sessionId: String, extraResults: Map = emptyMap()) { + post(MCP_PATH) { + val body = call.receiveText() + val parsed = Json.parseToJsonElement(body).jsonObject + val method = parsed["method"]?.jsonPrimitive?.content + val idJson = parsed["id"]?.toString() ?: "null" + call.response.header(SESSION_ID_HEADER, sessionId) + when { + method == "initialize" -> call.respondText( + text = jsonRpcResult(idJson, INITIALIZE_RESULT), + contentType = ContentType.Application.Json, + status = HttpStatusCode.OK, + ) + + method == "notifications/initialized" -> call.respond(HttpStatusCode.Accepted) + + method != null && method in extraResults -> call.respondText( + text = jsonRpcResult(idJson, extraResults.getValue(method)), + contentType = ContentType.Application.Json, + status = HttpStatusCode.OK, + ) + + else -> call.respond(HttpStatusCode.NotFound) + } + } + } + + private suspend fun runWithServer(routes: Route.() -> Unit, block: suspend (url: String) -> Unit) { + val server = embeddedServer(ServerCIO, host = HOST, port = 0) { + routing { routes() } + }.startSuspend(wait = false) + try { + val port = server.actualPort() + block("http://$HOST:$port$MCP_PATH") + } finally { + server.stopSuspend(1000, 2000) + } + } + + private fun newClient(): Client = Client( + clientInfo = Implementation(name = "client1", version = "1.0.0"), + options = ClientOptions(capabilities = ClientCapabilities()), + ) + + private fun newHttpClient(): HttpClient = HttpClient(ClientCIO) { + install(SSE) + } + + private fun jsonRpcResult(idJson: String, resultJson: String): String = + """{"jsonrpc":"2.0","id":$idJson,"result":$resultJson}""" + + private fun sseEvent(id: String, data: String): String = buildString { + append("event: message\n") + append("id: $id\n") + append("data: $data\n\n") + } +} diff --git a/kotlin-sdk-client/build.gradle.kts b/kotlin-sdk-client/build.gradle.kts index ae7a3149a..fbb130088 100644 --- a/kotlin-sdk-client/build.gradle.kts +++ b/kotlin-sdk-client/build.gradle.kts @@ -55,7 +55,6 @@ kotlin { implementation(libs.ktor.client.apache5) implementation(libs.mockk) implementation(libs.junit.jupiter.params) - implementation(libs.mokksy) implementation(dependencies.platform(libs.netty.bom)) runtimeOnly(libs.slf4j.simple) } diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt index 8810552a8..b380f64a6 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt @@ -478,6 +478,79 @@ class StreamableHttpClientTransportTest { } } + @Test + fun testSkipEmptySSE() = runTest { + val transport = createTransport { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + // Event 1: empty data — must be skipped without error + appendLine("id: a") + appendLine("data:") + appendLine() + // Event 2: whitespace-only data — must be skipped without error + appendLine("id: b") + appendLine("data: \t ") + appendLine() + // Event 3: multi-line data lines that together form a valid JSON-RPC response + appendLine("id: c") + appendLine("event: message") + appendLine("data: {") + appendLine("""data: "jsonrpc":"2.0",""") + appendLine("""data: "id":"test-1",""") + appendLine("""data: "result":{""") + appendLine("""data: "protocolVersion":"2025-06-18",""") + appendLine("""data: "capabilities":{},""") + appendLine("""data: "serverInfo":{"name":"server","version":"1.0.0"}""") + appendLine("data: }") + appendLine("data: }") + appendLine() + } + + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf( + HttpHeaders.ContentType, + ContentType.Text.EventStream.toString(), + ), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val messageReceived = CompletableDeferred() + + transport.onMessage { message -> + receivedMessages.add(message) + if (!messageReceived.isCompleted) { + messageReceived.complete(Unit) + } + } + + transport.start() + + transport.send( + JSONRPCRequest( + id = "test-1", + method = "initialize", + params = buildJsonObject { }, + ), + ) + + eventually { + messageReceived.await() + } + + receivedMessages shouldHaveSize 1 + val response = receivedMessages[0] + response.shouldBeInstanceOf() + response.id shouldBe RequestId.StringId("test-1") + + transport.close() + } + @Test fun testInlineSSEInResponse() = runTest { val transport = createTransport { request -> diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt deleted file mode 100644 index 4a04e8163..000000000 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.client - -import io.ktor.client.HttpClient -import io.ktor.client.engine.apache5.Apache5 -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logging -import io.ktor.client.plugins.sse.SSE -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.TestInstance - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal abstract class AbstractStreamableHttpClientTest { - - // start mokksy on random port - protected val mockMcp: MockMcp = MockMcp(verbose = true) - - @AfterEach - fun afterEach() { - mockMcp.verifyNoUnexpectedRequests() - } - - @AfterAll - fun afterAll() { - mockMcp.verifyNoUnmatchedStubs() - mockMcp.close() - } - - protected suspend fun connect(client: Client) { - client.connect( - StreamableHttpClientTransport( - url = mockMcp.url, - client = HttpClient(Apache5) { - install(SSE) - install(Logging) { - level = LogLevel.ALL - } - }, - ), - ) - } -} diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt deleted file mode 100644 index de9d2b3b5..000000000 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt +++ /dev/null @@ -1,237 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.client - -import dev.mokksy.mokksy.BuildingStep -import dev.mokksy.mokksy.Mokksy -import dev.mokksy.mokksy.StubConfiguration -import dev.mokksy.mokksy.shutdown -import dev.mokksy.mokksy.start -import io.ktor.http.ContentType -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.sse.ServerSentEvent -import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest -import io.modelcontextprotocol.kotlin.sdk.types.RequestId -import kotlinx.coroutines.flow.Flow -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.putJsonObject - -const val MCP_SESSION_ID_HEADER = "MCP-Session-Id" - -internal class MockMcp(verbose: Boolean = false) : AutoCloseable { - - private val mokksy: Mokksy = Mokksy(verbose = verbose).apply { - start() - } - - fun verifyNoUnexpectedRequests() { - mokksy.verifyNoUnexpectedRequests() - } - - fun verifyNoUnmatchedStubs() { - mokksy.verifyNoUnmatchedStubs() - } - - val url = "${mokksy.baseUrl()}/mcp" - - fun onInitialize( - clientName: String? = null, - sessionId: String, - protocolVersion: String = "2025-03-26", - serverName: String = "Mock MCP Server", - serverVersion: String = "1.0.0", - capabilities: JsonObject = buildJsonObject { - putJsonObject("tools") { - put("listChanged", JsonPrimitive(false)) - } - }, - ) { - val predicates = if (clientName != null) { - arrayOf<(JSONRPCRequest?) -> Boolean>({ - it?.params?.jsonObject - ?.get("clientInfo")?.jsonObject - ?.get("name")?.jsonPrimitive - ?.contentOrNull == clientName - }) - } else { - emptyArray() - } - - handleWithResult( - jsonRpcMethod = "initialize", - sessionId = sessionId, - bodyPredicates = predicates, - // language=json - result = """ - { - "capabilities": $capabilities, - "protocolVersion": "$protocolVersion", - "serverInfo": { - "name": "$serverName", - "version": "$serverVersion" - }, - "_meta": { - "foo": "bar" - } - } - """.trimIndent(), - ) - } - - fun onJSONRPCRequest( - httpMethod: HttpMethod = HttpMethod.Post, - jsonRpcMethod: String, - expectedSessionId: String? = null, - vararg bodyPredicates: (JSONRPCRequest) -> Boolean, - ): BuildingStep = mokksy.method( - configuration = StubConfiguration(removeAfterMatch = true), - httpMethod = httpMethod, - requestType = JSONRPCRequest::class, - ) { - path("/mcp") - expectedSessionId?.let { - containsHeader(MCP_SESSION_ID_HEADER, it) - } - bodyMatchesPredicate( - description = "JSON-RPC version is '2.0'", - predicate = - { - it!!.jsonrpc == "2.0" - }, - ) - bodyMatchesPredicate( - description = "JSON-RPC Method should be '$jsonRpcMethod'", - predicate = - { - it!!.method == jsonRpcMethod - }, - ) - bodyPredicates.forEach { predicate -> - bodyMatchesPredicate(predicate = { predicate.invoke(it!!) }) - } - } - - fun handleWithResult( - httpMethod: HttpMethod = HttpMethod.Post, - jsonRpcMethod: String, - expectedSessionId: String? = null, - sessionId: String, - contentType: ContentType = ContentType.Application.Json, - statusCode: HttpStatusCode = HttpStatusCode.OK, - vararg bodyPredicates: (JSONRPCRequest) -> Boolean, - result: () -> JsonObject, - ) { - onJSONRPCRequest( - httpMethod = httpMethod, - jsonRpcMethod = jsonRpcMethod, - expectedSessionId = expectedSessionId, - bodyPredicates = bodyPredicates, - ) respondsWith { - val requestId = when (val id = request.body().id) { - is RequestId.NumberId -> id.value.toString() - is RequestId.StringId -> "\"${id.value}\"" - } - val resultObject = result.invoke() - // language=json - body = """ - { - "jsonrpc": "2.0", - "id": $requestId, - "result": $resultObject - } - """.trimIndent() - this.contentType = contentType - headers += MCP_SESSION_ID_HEADER to sessionId - httpStatus = statusCode - } - } - - fun handleWithResult( - httpMethod: HttpMethod = HttpMethod.Post, - jsonRpcMethod: String, - expectedSessionId: String? = null, - sessionId: String, - contentType: ContentType = ContentType.Application.Json, - statusCode: HttpStatusCode = HttpStatusCode.OK, - vararg bodyPredicates: (JSONRPCRequest) -> Boolean, - result: String, - ) { - handleWithResult( - httpMethod = httpMethod, - jsonRpcMethod = jsonRpcMethod, - expectedSessionId = expectedSessionId, - sessionId = sessionId, - contentType = contentType, - statusCode = statusCode, - bodyPredicates = bodyPredicates, - result = { - Json.parseToJsonElement(result).jsonObject - }, - ) - } - - fun handleJSONRPCRequest( - httpMethod: HttpMethod = HttpMethod.Post, - jsonRpcMethod: String, - expectedSessionId: String? = null, - sessionId: String, - contentType: ContentType = ContentType.Application.Json, - statusCode: HttpStatusCode = HttpStatusCode.OK, - vararg bodyPredicates: (JSONRPCRequest?) -> Boolean, - bodyBuilder: () -> String = { "" }, - ) { - onJSONRPCRequest( - httpMethod = httpMethod, - jsonRpcMethod = jsonRpcMethod, - expectedSessionId = expectedSessionId, - bodyPredicates = bodyPredicates, - ) respondsWith { - body = bodyBuilder.invoke() - this.contentType = contentType - headers += MCP_SESSION_ID_HEADER to sessionId - httpStatus = statusCode - } - } - - fun onSubscribe(httpMethod: HttpMethod = HttpMethod.Post, sessionId: String): BuildingStep = mokksy.method( - httpMethod = httpMethod, - name = "MCP GETs", - requestType = Any::class, - ) { - path("/mcp") - containsHeader(MCP_SESSION_ID_HEADER, sessionId) - containsHeader("Accept", "application/json,text/event-stream") - containsHeader("Cache-Control", "no-store") - } - - fun handleSubscribeWithGet(sessionId: String, block: () -> Flow) { - onSubscribe( - httpMethod = HttpMethod.Get, - sessionId = sessionId, - ) respondsWithSseStream { - headers += MCP_SESSION_ID_HEADER to sessionId - this.flow = block.invoke() - } - } - - fun mockUnsubscribeRequest(sessionId: String) { - mokksy.delete( - configuration = StubConfiguration(removeAfterMatch = true), - requestType = JSONRPCRequest::class, - ) { - path("/mcp") - containsHeader(MCP_SESSION_ID_HEADER, sessionId) - } respondsWith { - body = null - } - } - - override fun close() { - mokksy.shutdown() - } -} diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt deleted file mode 100644 index 43be8c93e..000000000 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt +++ /dev/null @@ -1,293 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.client - -import io.kotest.matchers.collections.shouldContain -import io.kotest.matchers.shouldBe -import io.ktor.http.ContentType -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.sse.ServerSentEvent -import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities -import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject -import io.modelcontextprotocol.kotlin.sdk.types.Implementation -import io.modelcontextprotocol.kotlin.sdk.types.Tool -import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonObject -import org.junit.jupiter.api.TestInstance -import kotlin.test.Test -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid - -@OptIn(ExperimentalUuidApi::class) -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class StreamableHttpClientTest : AbstractStreamableHttpClientTest() { - - @Test - fun `Should skip empty SSE`(): Unit = runBlocking { - val client = Client( - clientInfo = Implementation( - name = "client1", - version = "1.0.0", - ), - options = ClientOptions( - capabilities = ClientCapabilities(), - ), - ) - val sessionId = Uuid.random().toString() - - mockMcp.onJSONRPCRequest( - httpMethod = HttpMethod.Post, - jsonRpcMethod = "initialize", - ).respondsWithStream { - headers += MCP_SESSION_ID_HEADER to sessionId - flow = flowOf( - "id: ${Uuid.random()}\n", - "data:\n", // empty data - "\n", - "id: ${Uuid.random()}\n", - "data: \t \n", // tabs and spaces - "\n", - "id: ${Uuid.random()}\n", - "event: message\n", - // multiline data - "data: {\n", - "data: \"result\":{\n" + - "data: \"protocolVersion\":\"2025-06-18\",\n" + - "data: \"capabilities\":{},\n" + - "data: \"serverInfo\":{\"name\":\"simple-streamable-http-server\",\"version\":\"1.0.0\"}\n" + - "data: },\n" + - "data: \"jsonrpc\":\"2.0\",\n" + - "data: \"id\":\"7ce065b0678f49e5b04ce5a0fcc7d518\"\n" + - "data: }\n", - "\n", - ) - } - - mockMcp.handleJSONRPCRequest( - jsonRpcMethod = "notifications/initialized", - expectedSessionId = sessionId, - sessionId = sessionId, - statusCode = HttpStatusCode.Accepted, - ) - - mockMcp.handleSubscribeWithGet(sessionId) { - emptyFlow() - } - - connect(client) - } - - @Test - fun `test streamableHttpClient`() = runBlocking { - val client = Client( - clientInfo = Implementation( - name = "client1", - version = "1.0.0", - ), - options = ClientOptions( - capabilities = ClientCapabilities(), - ), - ) - - val sessionId = Uuid.random().toString() - - mockMcp.onInitialize( - clientName = "client1", - sessionId = sessionId, - ) - - mockMcp.handleJSONRPCRequest( - jsonRpcMethod = "notifications/initialized", - expectedSessionId = sessionId, - sessionId = sessionId, - statusCode = HttpStatusCode.Accepted, - ) - - mockMcp.handleSubscribeWithGet(sessionId) { - flow { - delay(500.milliseconds) - emit( - ServerSentEvent( - event = "message", - id = "1", - //language=json - data = - """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""", - ), - ) - delay(200.milliseconds) - emit( - ServerSentEvent( - //language=json - data = - """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""", - ), - ) - } - } - - // TODO: how to get notifications via Client API? - - mockMcp.handleWithResult( - jsonRpcMethod = "tools/list", - sessionId = sessionId, - // language=json - result = """ - { - "tools": [ - { - "name": "get_weather", - "title": "Weather Information Provider", - "description": "Get current weather information for a location", - "inputSchema": { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "City name or zip code" - } - }, - "required": ["location"] - }, - "outputSchema": { - "type": "object", - "properties": { - "temperature": { - "type": "number", - "description": "Temperature, Celsius" - } - }, - "required": ["temperature"] - }, - "_meta": {} - } - ] - } - """.trimIndent(), - ) - - connect(client) - - val listToolsResult = client.listTools() - - listToolsResult.tools shouldContain Tool( - name = "get_weather", - title = "Weather Information Provider", - description = "Get current weather information for a location", - inputSchema = ToolSchema( - properties = buildJsonObject { - putJsonObject("location") { - put("type", "string") - put("description", "City name or zip code") - } - }, - required = listOf("location"), - ), - outputSchema = ToolSchema( - properties = buildJsonObject { - putJsonObject("temperature") { - put("type", "number") - put("description", "Temperature, Celsius") - } - }, - required = listOf("temperature"), - ), - annotations = null, - meta = EmptyJsonObject, - ) - - client.close() - } - - @Test - fun `terminateSession sends DELETE request`() = runBlocking { - val client = Client( - clientInfo = Implementation(name = "client1", version = "1.0.0"), - options = ClientOptions(capabilities = ClientCapabilities()), - ) - val sessionId = Uuid.random().toString() - - mockMcp.onInitialize(clientName = "client1", sessionId = sessionId) - mockMcp.handleJSONRPCRequest( - jsonRpcMethod = "notifications/initialized", - expectedSessionId = sessionId, - sessionId = sessionId, - statusCode = HttpStatusCode.Accepted, - ) - mockMcp.handleSubscribeWithGet(sessionId) { emptyFlow() } - - connect(client) - - mockMcp.mockUnsubscribeRequest(sessionId = sessionId) - (client.transport as StreamableHttpClientTransport).terminateSession() - (client.transport as StreamableHttpClientTransport).sessionId shouldBe null - - client.close() - } - - @Test - fun `handle MethodNotAllowed`() = runBlocking { - checkSupportNonStreamingResponse( - ContentType.Text.EventStream, - HttpStatusCode.MethodNotAllowed, - ) - } - - @Test - fun `handle non-streaming response`() = runBlocking { - checkSupportNonStreamingResponse( - ContentType.Application.Json, - HttpStatusCode.OK, - ) - } - - private suspend fun checkSupportNonStreamingResponse(contentType: ContentType, statusCode: HttpStatusCode) { - val sessionId = "SID_${Uuid.random().toHexString()}" - val clientName = "client-${Uuid.random().toHexString()}" - val client = Client( - clientInfo = Implementation(name = clientName, version = "1.0.0"), - options = ClientOptions( - capabilities = ClientCapabilities(), - ), - ) - - mockMcp.onInitialize(clientName = clientName, sessionId = sessionId) - - mockMcp.handleJSONRPCRequest( - jsonRpcMethod = "notifications/initialized", - expectedSessionId = sessionId, - sessionId = sessionId, - statusCode = HttpStatusCode.Accepted, - ) - - mockMcp.onSubscribe( - httpMethod = HttpMethod.Get, - sessionId = sessionId, - ) respondsWith { - headers += MCP_SESSION_ID_HEADER to sessionId - body = null - httpStatus = statusCode - this.contentType = contentType - } - - mockMcp.handleWithResult(jsonRpcMethod = "ping", sessionId = sessionId) { - buildJsonObject {} - } - - connect(client) - - delay(1.seconds) - - client.ping() // connection is still alive - - client.close() - } -} From ebc53f0ded3a309dcd6458bca16fcb52450b11d2 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 18 May 2026 13:22:26 +0200 Subject: [PATCH 140/187] feat!: capabilities for tasks, elicitation (#732) closes #544 Replaces opaque `JsonObject?` modelling of `elicitation` with a typed `ClientCapabilities.Elicitation` (`form`, `url`), and adds the new `tasks` capability on both `ClientCapabilities` and `ServerCapabilities` (with `list`, `cancel`, per-request-type augmentation). Wires capability assertions for `tasks/*` methods and `notifications/tasks/status` into `Client` and `ServerSession` ## How Has This Been Tested? unit tests ## Breaking Changes yes now elicitation is a specific class in capabilities ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .../kotlin/sdk/client/ClientTest.kt | 4 +- .../kotlin/sdk/client/Client.kt | 31 ++ .../sdk/client/ClientAssertCapabilityTest.kt | 152 ++++++++++ kotlin-sdk-core/api/kotlin-sdk-core.api | 281 ++++++++++++++++-- .../kotlin/sdk/types/capabilities.dsl.kt | 22 +- .../kotlin/sdk/types/capabilities.kt | 122 +++++++- .../kotlin/sdk/types/CapabilitiesTest.kt | 121 +++++++- .../sdk/types/dsl/CapabilitiesDslTest.kt | 11 +- .../kotlin/sdk/types/dsl/InitializeDslTest.kt | 12 +- .../kotlin/sdk/server/ServerSession.kt | 34 +++ .../ServerSessionAssertCapabilityTest.kt | 147 +++++++++ 11 files changed, 872 insertions(+), 65 deletions(-) create mode 100644 kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt create mode 100644 kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionAssertCapabilityTest.kt diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index e5f66383a..5c00b235c 100644 --- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -1058,7 +1058,7 @@ class ClientTest { Implementation(name = "test client", version = "1.0"), ClientOptions( capabilities = ClientCapabilities( - elicitation = EmptyJsonObject, + elicitation = ClientCapabilities.Elicitation(), ), ), ) @@ -1270,7 +1270,7 @@ class ClientTest { ): Pair = kotlinx.coroutines.coroutineScope { val client = Client( Implementation(name = "test client", version = "1.0"), - ClientOptions(capabilities = ClientCapabilities(elicitation = EmptyJsonObject)), + ClientOptions(capabilities = ClientCapabilities(elicitation = ClientCapabilities.Elicitation())), ) client.setElicitationHandler(handler) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index d71c09182..ee865afe5 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -174,6 +174,7 @@ public open class Client(private val clientInfo: Implementation, options: Client "prompts" -> caps?.prompts != null "resources" -> caps?.resources != null "tools" -> caps?.tools != null + "tasks" -> caps?.tasks != null else -> true } @@ -267,6 +268,12 @@ public open class Client(private val clientInfo: Implementation, options: Client } } + Method.Defined.TasksGet, + Method.Defined.TasksResult, + Method.Defined.TasksList, + Method.Defined.TasksCancel, + -> assertTasksCapabilityForMethod(method) + Method.Defined.Initialize, Method.Defined.Ping -> { // No specific capability required } @@ -277,6 +284,24 @@ public open class Client(private val clientInfo: Implementation, options: Client } } + private fun assertTasksCapabilityForMethod(method: Method) { + val tasks = serverCapabilities?.tasks + ?: error("Server does not support tasks (required for $method)") + when (method) { + Method.Defined.TasksList -> checkNotNull(tasks.list) { + "Server does not support listing tasks (required for $method)" + } + + Method.Defined.TasksCancel -> checkNotNull(tasks.cancel) { + "Server does not support cancelling tasks (required for $method)" + } + + else -> { + // TasksGet, TasksResult: base tasks capability suffices. + } + } + } + override fun assertNotificationCapability(method: Method) { when (method) { Method.Defined.NotificationsRootsListChanged -> { @@ -285,6 +310,12 @@ public open class Client(private val clientInfo: Implementation, options: Client } } + Method.Defined.NotificationsTasksStatus -> { + checkNotNull(capabilities.tasks) { + "Client does not support tasks (required for $method)" + } + } + Method.Defined.NotificationsInitialized, Method.Defined.NotificationsCancelled, Method.Defined.NotificationsProgress, diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt new file mode 100644 index 000000000..2f322ec90 --- /dev/null +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt @@ -0,0 +1,152 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFailsWith + +/** + * Tests for the protected helper [Client.assertCapability]. + * + * `assertCapability(capability, method)` is `protected`, so this suite uses + * a small subclass [TestClient] that re-exports it via [TestClient.exposedAssertCapability]. + * Server capabilities are seeded by completing the initialization handshake through + * [CapabilitiesTransport], which replays a configurable [ServerCapabilities] in its + * `initialize` response. + */ +class ClientAssertCapabilityTest { + + @Test + fun `assertCapability tasks throws when server has no tasks capability`() = runTest { + val client = newTestClient(serverCapabilities = ServerCapabilities()) + val ex = assertFailsWith { + client.exposedAssertCapability("tasks", "tasks/list") + } + ex.message.orEmpty() shouldContain "Server does not support tasks" + } + + @Test + fun `TasksGet throws when server has no tasks capability`() = runTest { + val client = newTestClient(serverCapabilities = ServerCapabilities()) + val ex = assertFailsWith { + client.exposedAssertCapabilityForMethod(Method.Defined.TasksGet) + } + ex.message.orEmpty() shouldContain "Server does not support tasks" + } + + @Test + fun `TasksGet does not throw when server declared tasks`() = runTest { + val client = newTestClient( + serverCapabilities = ServerCapabilities(tasks = ServerCapabilities.Tasks()), + ) + client.exposedAssertCapabilityForMethod(Method.Defined.TasksGet) + } + + @Test + fun `TasksList throws when server tasks list is null`() = runTest { + val client = newTestClient( + serverCapabilities = ServerCapabilities(tasks = ServerCapabilities.Tasks()), + ) + val ex = assertFailsWith { + client.exposedAssertCapabilityForMethod(Method.Defined.TasksList) + } + ex.message.orEmpty() shouldContain "Server does not support listing tasks" + } + + @Test + fun `TasksCancel throws when server tasks cancel is null`() = runTest { + val client = newTestClient( + serverCapabilities = ServerCapabilities(tasks = ServerCapabilities.Tasks()), + ) + val ex = assertFailsWith { + client.exposedAssertCapabilityForMethod(Method.Defined.TasksCancel) + } + ex.message.orEmpty() shouldContain "Server does not support cancelling tasks" + } + + @Test + fun `NotificationsTasksStatus throws when client has no tasks capability`() = runTest { + val client = newTestClient(clientCapabilities = ClientCapabilities()) + val ex = assertFailsWith { + client.exposedAssertNotificationCapability(Method.Defined.NotificationsTasksStatus) + } + ex.message.orEmpty() shouldContain "Client does not support tasks" + } + + private suspend fun newTestClient( + serverCapabilities: ServerCapabilities = ServerCapabilities(), + clientCapabilities: ClientCapabilities = ClientCapabilities(), + ): TestClient { + val client = TestClient( + Implementation("test-client", "1.0.0"), + ClientOptions(capabilities = clientCapabilities), + ) + val transport = CapabilitiesTransport(serverCapabilities) + client.connect(transport) + return client + } + + /** + * Test-only [Client] subclass that exposes the protected + * [Client.assertCapability], [Client.assertCapabilityForMethod], and + * [Client.assertNotificationCapability] helpers to the test code. + */ + private class TestClient(clientInfo: Implementation, options: ClientOptions = ClientOptions()) : + Client(clientInfo, options) { + fun exposedAssertCapability(capability: String, method: String): Unit = assertCapability(capability, method) + fun exposedAssertCapabilityForMethod(method: Method): Unit = assertCapabilityForMethod(method) + fun exposedAssertNotificationCapability(method: Method): Unit = assertNotificationCapability(method) + } + + /** + * Minimal in-memory [Transport] that responds to `initialize` with a configurable + * [ServerCapabilities]. Other JSON-RPC traffic (e.g. the `notifications/initialized` + * sent by [Client.connect]) is silently consumed. + */ + private class CapabilitiesTransport(private val serverCapabilities: ServerCapabilities) : Transport { + private var onMessageBlock: (suspend (JSONRPCMessage) -> Unit)? = null + private var onCloseBlock: (() -> Unit)? = null + + override suspend fun start() = Unit + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + if (message is JSONRPCRequest && message.method == "initialize") { + onMessageBlock?.invoke( + JSONRPCResponse( + id = message.id, + result = InitializeResult( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = serverCapabilities, + serverInfo = Implementation("mock-server", "1.0.0"), + ), + ), + ) + } + } + + override suspend fun close() { + onCloseBlock?.invoke() + } + + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { + onMessageBlock = block + } + + override fun onClose(block: () -> Unit) { + onCloseBlock = block + } + + override fun onError(block: (Throwable) -> Unit) = Unit + } +} diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index c843aea06..b588eef2a 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -607,23 +607,25 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotificatio public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Companion; public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; - public final fun component3 ()Lkotlinx/serialization/json/JsonObject; - public final fun component4 ()Lkotlinx/serialization/json/JsonObject; - public final fun component5 ()Ljava/util/Map; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; + public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks; + public final fun component5 ()Lkotlinx/serialization/json/JsonObject; + public final fun component6 ()Ljava/util/Map; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; public fun equals (Ljava/lang/Object;)Z - public final fun getElicitation ()Lkotlinx/serialization/json/JsonObject; + public final fun getElicitation ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; public final fun getExperimental ()Lkotlinx/serialization/json/JsonObject; public final fun getExtensions ()Ljava/util/Map; public final fun getRoots ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots; public final fun getSampling ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; + public final fun getTasks ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -640,11 +642,42 @@ public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapa } public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Companion { - public final fun getElicitation ()Lkotlinx/serialization/json/JsonObject; + public final fun getElicitation ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; public final fun getSampling ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling; public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; + public fun equals (Ljava/lang/Object;)Z + public final fun getForm ()Lkotlinx/serialization/json/JsonObject; + public final fun getUrl ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation; + 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/types/ClientCapabilities$Elicitation;)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/types/ClientCapabilities$Elicitation$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots$Companion; public fun ()V @@ -705,11 +738,132 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$S public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests; + public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks; + public fun equals (Ljava/lang/Object;)Z + public final fun getCancel ()Lkotlinx/serialization/json/JsonObject; + public final fun getList ()Lkotlinx/serialization/json/JsonObject; + public final fun getRequests ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks; + 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/types/ClientCapabilities$Tasks;)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/types/ClientCapabilities$Tasks$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling; + public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests; + public fun equals (Ljava/lang/Object;)Z + public final fun getElicitation ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation; + public final fun getSampling ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests; + 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/types/ClientCapabilities$Tasks$Requests;)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/types/ClientCapabilities$Tasks$Requests$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation$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/types/ClientCapabilities$Tasks$Requests$Elicitation; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation; + public fun equals (Ljava/lang/Object;)Z + public final fun getCreate ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation; + 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/types/ClientCapabilities$Tasks$Requests$Elicitation;)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/types/ClientCapabilities$Tasks$Requests$Elicitation$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling$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/types/ClientCapabilities$Tasks$Requests$Sampling; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling; + public fun equals (Ljava/lang/Object;)Z + public final fun getCreateMessage ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling; + 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/types/ClientCapabilities$Tasks$Requests$Sampling;)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/types/ClientCapabilities$Tasks$Requests$Sampling$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilitiesBuilder { public fun ()V public final fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; - public final fun elicitation (Lkotlin/jvm/functions/Function1;)V - public final fun elicitation (Lkotlinx/serialization/json/JsonObject;)V + public final fun elicitation (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;)V public final fun experimental (Lkotlin/jvm/functions/Function1;)V public final fun experimental (Lkotlinx/serialization/json/JsonObject;)V public final fun extensions (Ljava/util/Map;)V @@ -4303,17 +4457,18 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Sampling_dslKt { public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Companion; public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; public final fun component4 ()Lkotlinx/serialization/json/JsonObject; public final fun component5 ()Lkotlinx/serialization/json/JsonObject; - public final fun component6 ()Lkotlinx/serialization/json/JsonObject; - public final fun component7 ()Ljava/util/Map; - public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; - public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks; + public final fun component7 ()Lkotlinx/serialization/json/JsonObject; + public final fun component8 ()Ljava/util/Map; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; public fun equals (Ljava/lang/Object;)Z public final fun getCompletions ()Lkotlinx/serialization/json/JsonObject; public final fun getExperimental ()Lkotlinx/serialization/json/JsonObject; @@ -4321,6 +4476,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities { public final fun getLogging ()Lkotlinx/serialization/json/JsonObject; public final fun getPrompts ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts; public final fun getResources ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources; + public final fun getTasks ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks; public final fun getTools ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -4403,6 +4559,97 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$R public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Companion; + public fun ()V + public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;)V + public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/serialization/json/JsonObject; + public final fun component2 ()Lkotlinx/serialization/json/JsonObject; + public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests; + public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks; + public fun equals (Ljava/lang/Object;)Z + public final fun getCancel ()Lkotlinx/serialization/json/JsonObject; + public final fun getList ()Lkotlinx/serialization/json/JsonObject; + public final fun getRequests ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks; + 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/types/ServerCapabilities$Tasks;)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/types/ServerCapabilities$Tasks$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Companion; + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools; + public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests; + public fun equals (Ljava/lang/Object;)Z + public final fun getTools ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests; + 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/types/ServerCapabilities$Tasks$Requests;)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/types/ServerCapabilities$Tasks$Requests$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools$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/types/ServerCapabilities$Tasks$Requests$Tools; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools; + public fun equals (Ljava/lang/Object;)Z + public final fun getCall ()Lkotlinx/serialization/json/JsonObject; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools; + 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/types/ServerCapabilities$Tasks$Requests$Tools;)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/types/ServerCapabilities$Tasks$Requests$Tools$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools$Companion; public fun ()V diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt index 75e923b2d..0d8b2b7c8 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt @@ -38,7 +38,7 @@ import kotlinx.serialization.json.buildJsonObject public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { private var sampling: ClientCapabilities.Sampling? = null private var roots: ClientCapabilities.Roots? = null - private var elicitation: JsonObject? = null + private var elicitation: ClientCapabilities.Elicitation? = null private var extensions: Map? = null private var experimental: JsonObject? = null @@ -47,7 +47,7 @@ public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { * * Pass `ClientCapabilities.Sampling()` to enable base sampling with no sub-capabilities. * Construct `ClientCapabilities.Sampling(tools = EmptyJsonObject, context = EmptyJsonObject)` - * directly to enable SEP-1577 sub-capabilities. + * directly to enable tools/context sub-capabilities. * * Example: * ```kotlin @@ -99,26 +99,10 @@ public class ClientCapabilitiesBuilder @PublishedApi internal constructor() { * * @param value The elicitation capability configuration */ - public fun elicitation(value: JsonObject) { + public fun elicitation(value: ClientCapabilities.Elicitation) { this.elicitation = value } - /** - * Indicates that the client supports elicitation from the server with custom configuration. - * - * Example: - * ```kotlin - * capabilities { - * elicitation { - * put("mode", JsonPrimitive("interactive")) - * } - * } - * ``` - * - * @param block Lambda for building the elicitation configuration - */ - public fun elicitation(block: JsonObjectBuilder.() -> Unit): Unit = elicitation(buildJsonObject(block)) - /** * Defines extensions that the client supports. * diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt index e13a75036..cd78b582a 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt @@ -41,9 +41,11 @@ public data class Implementation( * supports that capability. * * @property sampling Present if the client supports sampling from an LLM. - * Use [ClientCapabilities.Sampling] to configure SEP-1577 sub-capabilities (tools, context). + * Use [ClientCapabilities.Sampling] to configure tools/context sub-capabilities. * @property roots Present if the client supports listing roots. * @property elicitation Present if the client supports elicitation from the server. + * Use [ClientCapabilities.Elicitation] to configure form/url mode sub-capabilities. + * @property tasks Present if the client supports task-augmented requests, listing, or cancellation. * @property experimental Experimental, non-standard capabilities that the client supports. * Keys are capability names, values are capability-specific configuration objects. * @property extensions Optional extensions that the client supports. @@ -54,23 +56,33 @@ public data class Implementation( public data class ClientCapabilities( public val sampling: Sampling? = null, public val roots: Roots? = null, - public val elicitation: JsonObject? = null, + public val elicitation: Elicitation? = null, + public val tasks: Tasks? = null, public val experimental: JsonObject? = null, public val extensions: Map? = null, ) { /** - * Source-compatibility constructor retaining the pre-SEP-1577 `sampling: JsonObject?` - * shape. Any non-null [sampling] is converted to an empty [Sampling] (sub-capabilities - * cannot be recovered from the old opaque `JsonObject`). + * Source-compatibility constructor retaining the older `sampling: JsonObject?` / + * `elicitation: JsonObject?` shapes. Any non-null [sampling] is converted to an + * empty [Sampling], and any non-null [elicitation] is converted to an empty + * [Elicitation] (sub-capabilities cannot be recovered from the old opaque + * `JsonObject`). */ @Deprecated( - "ClientCapabilities.sampling is now typed. Pass a ClientCapabilities.Sampling? " + + "ClientCapabilities.sampling and ClientCapabilities.elicitation are now typed. " + + "Pass typed ClientCapabilities.Sampling? / ClientCapabilities.Elicitation? " + "instead of JsonObject?.", ReplaceWith( - "ClientCapabilities(sampling?.let { ClientCapabilities.Sampling() }, " + - "roots, elicitation, experimental, extensions)", + "ClientCapabilities(" + + "sampling?.let { ClientCapabilities.Sampling() }, " + + "roots, " + + "elicitation?.let { ClientCapabilities.Elicitation() }, " + + "tasks = null, " + + "experimental = experimental, " + + "extensions = extensions)", ), + level = DeprecationLevel.WARNING, ) public constructor( sampling: JsonObject?, @@ -81,7 +93,8 @@ public data class ClientCapabilities( ) : this( sampling = sampling?.let { Sampling() }, roots = roots, - elicitation = elicitation, + elicitation = elicitation?.let { Elicitation() }, + tasks = null, experimental = experimental, extensions = extensions, ) @@ -101,11 +114,11 @@ public data class ClientCapabilities( /** * @property sampling convenience value to enable the base sampling capability with no sub-capabilities - * @property elicitation convenience value to enable the elicitation capability + * @property elicitation convenience value to enable the elicitation capability with default form mode */ public companion object { public val sampling: Sampling = Sampling() - public val elicitation: JsonObject = EmptyJsonObject + public val elicitation: Elicitation = Elicitation() } /** @@ -120,6 +133,60 @@ public data class ClientCapabilities( */ @Serializable public data class Roots(val listChanged: Boolean? = null) + + /** + * Sub-capabilities for elicitation. + * + * An empty [Elicitation] (both fields null) is equivalent to declaring `form` mode only. + * + * @property form Present if the client supports form-mode elicitation + * (in-band structured data collection). + * @property url Present if the client supports url-mode elicitation + * (out-of-band interaction via URL navigation). + */ + @Serializable + public data class Elicitation(public val form: JsonObject? = null, public val url: JsonObject? = null) + + /** + * Sub-capabilities for tasks (client side). + * + * Declares which client-side requests can be augmented with task execution, + * and whether the client supports listing and cancelling tasks. + * + * @property list Present if the client supports the `tasks/list` operation. + * @property cancel Present if the client supports the `tasks/cancel` operation. + * @property requests Present if the client supports task-augmented sampling and/or elicitation requests. + */ + @Serializable + public data class Tasks( + public val list: JsonObject? = null, + public val cancel: JsonObject? = null, + public val requests: Requests? = null, + ) { + /** + * Task-augmentable client-side request categories. + * + * @property sampling Present if the client supports task-augmented `sampling/createMessage` requests. + * @property elicitation Present if the client supports task-augmented `elicitation/create` requests. + */ + @Serializable + public data class Requests( + public val sampling: Sampling? = null, + public val elicitation: Elicitation? = null, + ) { + /** + * @property createMessage Present if the client supports task-augmented `sampling/createMessage` requests. + */ + @Serializable + public data class Sampling(public val createMessage: JsonObject? = null) + + /** + * @property create Present if the client supports task-augmented `elicitation/create` requests. + */ + @Serializable + public data class Elicitation(public val create: JsonObject? = null) + } + } } /** @@ -137,6 +204,7 @@ public data class ClientCapabilities( * @property logging Present if the server supports sending log messages to the client. * @property completions Present if the server supports argument autocompletion suggestions. * Keys are capability names, values are capability-specific configuration objects. + * @property tasks Present if the server supports task-augmented requests, listing, or cancellation. * @property experimental Experimental, non-standard capabilities that the server supports. * @property extensions Optional extensions that the server supports. * Keys are extension identifiers (e.g., `"io.modelcontextprotocol/ui"`), @@ -149,6 +217,7 @@ public data class ServerCapabilities( val prompts: Prompts? = null, val logging: JsonObject? = null, val completions: JsonObject? = null, + val tasks: Tasks? = null, val experimental: JsonObject? = null, val extensions: Map? = null, ) { @@ -199,4 +268,35 @@ public data class ServerCapabilities( */ @Serializable public data class Prompts(val listChanged: Boolean? = null) + + /** + * Sub-capabilities for tasks (server side). + * + * Declares which server-side requests can be augmented with task execution, + * and whether the server supports listing and cancelling tasks. + * + * @property list Present if the server supports the `tasks/list` operation. + * @property cancel Present if the server supports the `tasks/cancel` operation. + * @property requests Present if the server supports task-augmented tool call requests. + */ + @Serializable + public data class Tasks( + public val list: JsonObject? = null, + public val cancel: JsonObject? = null, + public val requests: Requests? = null, + ) { + /** + * Task-augmentable server-side request categories. + * + * @property tools Present if the server supports task-augmented `tools/call` requests. + */ + @Serializable + public data class Requests(public val tools: Tools? = null) { + /** + * @property call Present if the server supports task-augmented `tools/call` requests. + */ + @Serializable + public data class Tools(public val call: JsonObject? = null) + } + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt index 8b2e4f849..06949a6a7 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt @@ -313,7 +313,7 @@ class CapabilitiesTest { assertEquals(ClientCapabilities.Sampling(), capabilities.sampling) assertEquals(true, capabilities.roots?.listChanged) - assertEquals(EmptyJsonObject, capabilities.elicitation) + assertEquals(ClientCapabilities.Elicitation(), capabilities.elicitation) } @Test @@ -804,4 +804,123 @@ class CapabilitiesTest { val caps = ClientCapabilities() caps.sampling shouldBe null } + + @Test + fun `should serialize ServerCapabilities with tasks`() { + val capabilities = ServerCapabilities( + tasks = ServerCapabilities.Tasks( + list = EmptyJsonObject, + cancel = EmptyJsonObject, + requests = ServerCapabilities.Tasks.Requests( + tools = ServerCapabilities.Tasks.Requests.Tools(call = EmptyJsonObject), + ), + ), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "tasks": { + "list": {}, + "cancel": {}, + "requests": { + "tools": { + "call": {} + } + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ServerCapabilities with tasks`() { + val json = """ + { + "tasks": { + "list": {}, + "requests": {"tools": {"call": {}}} + } + } + """.trimIndent() + val capabilities = verifyDeserialization(McpJson, json) + assertEquals(EmptyJsonObject, capabilities.tasks?.list) + assertNull(capabilities.tasks?.cancel) + assertEquals(EmptyJsonObject, capabilities.tasks?.requests?.tools?.call) + } + + @Test + fun `should serialize ClientCapabilities with tasks`() { + val capabilities = ClientCapabilities( + tasks = ClientCapabilities.Tasks( + list = EmptyJsonObject, + cancel = EmptyJsonObject, + requests = ClientCapabilities.Tasks.Requests( + sampling = ClientCapabilities.Tasks.Requests.Sampling(createMessage = EmptyJsonObject), + elicitation = ClientCapabilities.Tasks.Requests.Elicitation(create = EmptyJsonObject), + ), + ), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "tasks": { + "list": {}, + "cancel": {}, + "requests": { + "sampling": {"createMessage": {}}, + "elicitation": {"create": {}} + } + } + } + """.trimIndent(), + ) + } + + @Test + fun `should serialize ClientCapabilities with structured elicitation form and url`() { + val capabilities = ClientCapabilities( + elicitation = ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject), + ) + verifySerialization( + capabilities, + McpJson, + """ + { + "elicitation": { + "form": {}, + "url": {} + } + } + """.trimIndent(), + ) + } + + @Test + fun `should deserialize ClientCapabilities with elicitation and tasks`() { + val json = """ + { + "elicitation": {"form": {}, "url": {}}, + "tasks": { + "list": {}, + "cancel": {}, + "requests": { + "sampling": {"createMessage": {}}, + "elicitation": {"create": {}} + } + } + } + """.trimIndent() + val capabilities = verifyDeserialization(McpJson, json) + assertEquals(EmptyJsonObject, capabilities.elicitation?.form) + assertEquals(EmptyJsonObject, capabilities.elicitation?.url) + assertEquals(EmptyJsonObject, capabilities.tasks?.list) + assertEquals(EmptyJsonObject, capabilities.tasks?.cancel) + assertEquals(EmptyJsonObject, capabilities.tasks?.requests?.sampling?.createMessage) + assertEquals(EmptyJsonObject, capabilities.tasks?.requests?.elicitation?.create) + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt index ee2fe434c..25d9ee6d8 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt @@ -44,11 +44,7 @@ class CapabilitiesDslTest { capabilities { sampling(ClientCapabilities.Sampling(tools = EmptyJsonObject)) roots(listChanged = true) - elicitation { - put("mode", "interactive") - put("timeout", 30) - put("retries", 3) - } + elicitation(ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject)) experimental { put("customFeature", true) put("version", "1.0") @@ -74,9 +70,8 @@ class CapabilitiesDslTest { listChanged shouldBe true } elicitation shouldNotBeNull { - get("mode")?.jsonPrimitive?.content shouldBe "interactive" - get("timeout")?.jsonPrimitive?.content shouldBe "30" - get("retries")?.jsonPrimitive?.content shouldBe "3" + form shouldBe EmptyJsonObject + url shouldBe EmptyJsonObject } experimental shouldNotBeNull { get("customFeature")?.jsonPrimitive?.content shouldBe "true" diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt index 17e5ae61d..da2a4cb77 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt @@ -22,9 +22,7 @@ class InitializeDslTest { capabilities { sampling(ClientCapabilities.Sampling()) roots(listChanged = true) - elicitation { - put("mode", "interactive") - } + elicitation(ClientCapabilities.Elicitation(form = EmptyJsonObject)) experimental { put("custom", true) } @@ -46,7 +44,7 @@ class InitializeDslTest { request.params.capabilities.shouldNotBeNull { sampling shouldNotBeNull { } roots?.listChanged shouldBe true - elicitation?.get("mode")?.jsonPrimitive?.content shouldBe "interactive" + elicitation?.form shouldBe EmptyJsonObject experimental?.get("custom")?.jsonPrimitive?.content shouldBe "true" extensions shouldNotBeNull { get("io.modelcontextprotocol/ui") shouldBe EmptyJsonObject @@ -78,7 +76,7 @@ class InitializeDslTest { @Test fun `capabilities DSL should support direct typed values`() { val samplingValue = ClientCapabilities.Sampling(tools = EmptyJsonObject) - val elicitationObj = buildJsonObject { put("key", "value") } + val elicitationValue = ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject) val experimentalObj = buildJsonObject { put("key", "value") } val extensionsMap = mapOf( "io.modelcontextprotocol/ui" to buildJsonObject { put("key", "value") }, @@ -88,7 +86,7 @@ class InitializeDslTest { protocolVersion = "1.0" capabilities { sampling(samplingValue) - elicitation(elicitationObj) + elicitation(elicitationValue) experimental(experimentalObj) extensions(extensionsMap) } @@ -97,7 +95,7 @@ class InitializeDslTest { request.params.capabilities.shouldNotBeNull { sampling shouldBe samplingValue - elicitation shouldBe elicitationObj + elicitation shouldBe elicitationValue experimental shouldBe experimentalObj extensions shouldBe extensionsMap } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt index e7da26f62..d215bd511 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -172,6 +172,12 @@ public open class ServerSession( } } + Defined.TasksGet, + Defined.TasksResult, + Defined.TasksList, + Defined.TasksCancel, + -> assertTasksCapabilityForMethod(method) + Defined.Ping -> { // No specific capability required } @@ -182,6 +188,28 @@ public open class ServerSession( } } + private fun assertTasksCapabilityForMethod(method: Method) { + val tasks = clientCapabilities?.tasks + ?: error("Client does not support tasks (required for ${method.value})") + when (method) { + Defined.TasksList -> { + if (tasks.list == null) { + error("Client does not support listing tasks (required for ${method.value})") + } + } + + Defined.TasksCancel -> { + if (tasks.cancel == null) { + error("Client does not support cancelling tasks (required for ${method.value})") + } + } + + else -> { + // TasksGet, TasksResult: base tasks capability suffices. + } + } + } + /** * Asserts that the server can handle the specified notification method. * @@ -225,6 +253,12 @@ public open class ServerSession( } } + Defined.NotificationsTasksStatus -> { + if (serverCapabilities.tasks == null) { + error("Server does not support tasks (required for ${method.value})") + } + } + Defined.NotificationsCancelled, Defined.NotificationsProgress, -> { diff --git a/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionAssertCapabilityTest.kt b/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionAssertCapabilityTest.kt new file mode 100644 index 000000000..a172e965b --- /dev/null +++ b/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionAssertCapabilityTest.kt @@ -0,0 +1,147 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFailsWith + +/** + * Tests for the protected helpers [ServerSession.assertCapabilityForMethod] and + * [ServerSession.assertNotificationCapability]. + * + * Both helpers are `protected`, so this suite uses a small subclass [TestServerSession] + * that re-exports them via [TestServerSession.exposedAssertCapabilityForMethod] and + * [TestServerSession.exposedAssertNotificationCapability]. Client capabilities are + * seeded by replaying an [InitializeRequest] through [InitializeReplayTransport]: + * the transport drives the inbound side of the handshake on `start()`, which causes + * the session's internal [Method.Defined.Initialize] handler to populate + * [ServerSession.clientCapabilities]. + */ +class ServerSessionAssertCapabilityTest { + + @Test + fun `server TasksGet throws when client has no tasks capability`() = runTest { + val session = newTestServerSession(clientCapabilities = ClientCapabilities()) + val ex = assertFailsWith { + session.exposedAssertCapabilityForMethod(Method.Defined.TasksGet) + } + ex.message.orEmpty() shouldContain "Client does not support tasks" + } + + @Test + fun `server TasksGet succeeds when client declared tasks`() = runTest { + val session = newTestServerSession( + clientCapabilities = ClientCapabilities(tasks = ClientCapabilities.Tasks()), + ) + session.exposedAssertCapabilityForMethod(Method.Defined.TasksGet) + } + + @Test + fun `server TasksList throws when client tasks list is null`() = runTest { + val session = newTestServerSession( + clientCapabilities = ClientCapabilities(tasks = ClientCapabilities.Tasks()), + ) + val ex = assertFailsWith { + session.exposedAssertCapabilityForMethod(Method.Defined.TasksList) + } + ex.message.orEmpty() shouldContain "Client does not support listing tasks" + } + + @Test + fun `server TasksCancel throws when client tasks cancel is null`() = runTest { + val session = newTestServerSession( + clientCapabilities = ClientCapabilities(tasks = ClientCapabilities.Tasks()), + ) + val ex = assertFailsWith { + session.exposedAssertCapabilityForMethod(Method.Defined.TasksCancel) + } + ex.message.orEmpty() shouldContain "Client does not support cancelling tasks" + } + + @Test + fun `server NotificationsTasksStatus throws when server has no tasks capability`() = runTest { + val session = newTestServerSession(serverCapabilities = ServerCapabilities()) + val ex = assertFailsWith { + session.exposedAssertNotificationCapability(Method.Defined.NotificationsTasksStatus) + } + ex.message.orEmpty() shouldContain "Server does not support tasks" + } + + private suspend fun newTestServerSession( + clientCapabilities: ClientCapabilities = ClientCapabilities(), + serverCapabilities: ServerCapabilities = ServerCapabilities(), + ): TestServerSession { + val session = TestServerSession( + serverInfo = Implementation("test-server", "1.0.0"), + options = ServerOptions(capabilities = serverCapabilities), + instructions = null, + ) + val transport = InitializeReplayTransport(clientCapabilities) + session.connect(transport) + return session + } + + /** + * Test-only [ServerSession] subclass that exposes the protected + * [ServerSession.assertCapabilityForMethod] and + * [ServerSession.assertNotificationCapability] helpers to the test code. + */ + private class TestServerSession(serverInfo: Implementation, options: ServerOptions, instructions: String?) : + ServerSession(serverInfo, options, instructions) { + fun exposedAssertCapabilityForMethod(method: Method): Unit = assertCapabilityForMethod(method) + fun exposedAssertNotificationCapability(method: Method): Unit = assertNotificationCapability(method) + } + + /** + * Minimal in-memory [Transport] that drives the server-side initialize handshake. + * + * On [start], it dispatches an [InitializeRequest] carrying the configured + * [clientCapabilities] to the session via [onMessage]. The session's internal + * `initialize` handler runs synchronously and populates + * [ServerSession.clientCapabilities] before [start] returns. Other JSON-RPC + * traffic the session subsequently emits is silently consumed. + */ + private class InitializeReplayTransport(private val clientCapabilities: ClientCapabilities) : Transport { + private var onMessageBlock: (suspend (JSONRPCMessage) -> Unit)? = null + private var onCloseBlock: (() -> Unit)? = null + + override suspend fun start() { + val initializeRequest = InitializeRequest( + params = InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = clientCapabilities, + clientInfo = Implementation("mock-client", "1.0.0"), + ), + ).toJSON().copy(id = RequestId.NumberId(1)) + onMessageBlock?.invoke(initializeRequest) + } + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) = Unit + + override suspend fun close() { + onCloseBlock?.invoke() + } + + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { + onMessageBlock = block + } + + override fun onClose(block: () -> Unit) { + onCloseBlock = block + } + + override fun onError(block: (Throwable) -> Unit) = Unit + } +} From 97a429f42488fcd99e627f64d95d81a405add39f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:52:17 +0200 Subject: [PATCH 141/187] chore(deps): bump org.junit.jupiter:junit-jupiter-params from 5.14.3 to 5.14.4 in the testing group across 1 directory (#768) Bumps the testing group with 1 update in the / directory: [org.junit.jupiter:junit-jupiter-params](https://github.com/junit-team/junit-framework). Updates `org.junit.jupiter:junit-jupiter-params` from 5.14.3 to 5.14.4
    Release notes

    Sourced from org.junit.jupiter:junit-jupiter-params's releases.

    JUnit 5.14.4 = Platform 1.14.4 + Jupiter 5.14.4 + Vintage 5.14.4

    See Release Notes.

    Full Changelog: https://github.com/junit-team/junit-framework/compare/r5.14.3...r5.14.4

    Commits
    • 096fd69 Release 5.14.4
    • 11fd02b Remove JDK 24 (EOL)
    • 5b7f023 Fix integration tests on JDK 27
    • 18f842a Test against JDK 26 (GA) and JDK 27 (EA)
    • a5bf336 Polish 5.14.4 release notes
    • 7667c14 Fix race condition in NodeTestTask (#5427)
    • ac01f2d Include full display name in legacy XML reports (#5524)
    • fbd4a8e Include class template invocation index in legacy reporting names
    • 7d6f0c0 Only check PDF for GA releases
    • 14f534d Test 5.14.x against JDK 25 ga (#5623)
    • Additional commits viewable in compare view

    Most Recent Ignore Conditions Applied to This Pull Request | Dependency Name | Ignore Conditions | | --- | --- | | org.junit.jupiter:junit-jupiter-params | [>= 6.a, < 7] |
    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 5b04ca45d..2480fb03c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,7 @@ mockk = "1.14.9" serialization = "1.11.0" slf4j = "2.0.17" logback = "1.5.32" -junit = "5.14.3" +junit = "5.14.4" [libraries] # Plugins From 9764036ec51f4f1bbeb8e01649c99653fb4bb8b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 14:13:35 +0200 Subject: [PATCH 142/187] chore(deps): bump org.slf4j:slf4j-simple from 2.0.17 to 2.0.18 in the infrastructure group across 1 directory (#769) Bumps the infrastructure group with 1 update in the / directory: org.slf4j:slf4j-simple. Updates `org.slf4j:slf4j-simple` from 2.0.17 to 2.0.18 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 2480fb03c..bf36c70e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ ktor = "3.3.3" logging = "8.0.02" mockk = "1.14.9" serialization = "1.11.0" -slf4j = "2.0.17" +slf4j = "2.0.18" logback = "1.5.32" junit = "5.14.4" From af7c5e13174465ef75fef90be46b250f3e1992db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 15:18:30 +0200 Subject: [PATCH 143/187] chore(deps): bump the other-dependencies group across 5 directories with 3 updates (#770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the other-dependencies group with 3 updates in the /samples/kotlin-mcp-client directory: [gradle-wrapper](https://github.com/gradle/gradle), [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java) and org.slf4j:slf4j-simple. Bumps the other-dependencies group with 2 updates in the /samples/kotlin-mcp-server directory: [gradle-wrapper](https://github.com/gradle/gradle) and org.slf4j:slf4j-simple. Bumps the other-dependencies group with 1 update in the /samples/kotlinlang-mcp-server directory: [gradle-wrapper](https://github.com/gradle/gradle). Bumps the other-dependencies group with 2 updates in the /samples/simple-streamable-server directory: [gradle-wrapper](https://github.com/gradle/gradle) and org.slf4j:slf4j-simple. Bumps the other-dependencies group with 2 updates in the /samples/weather-stdio-server directory: [gradle-wrapper](https://github.com/gradle/gradle) and org.slf4j:slf4j-simple. Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Updates `com.anthropic:anthropic-java` from 2.31.0 to 2.32.0
    Release notes

    Sourced from com.anthropic:anthropic-java's releases.

    v2.32.0

    2.32.0 (2026-05-13)

    Full Changelog: v2.31.0...v2.32.0

    Features

    • api: Add BetaManagedAgentsSearchResultBlock types (253feaa)
    • api: Add support for cache diagnostics beta (7a32231)
    • mcp: add mcp tool helpers (#1161) (5d58812)

    Chores

    Changelog

    Sourced from com.anthropic:anthropic-java's changelog.

    2.32.0 (2026-05-13)

    Full Changelog: v2.31.0...v2.32.0

    Features

    • api: Add BetaManagedAgentsSearchResultBlock types (253feaa)
    • api: Add support for cache diagnostics beta (7a32231)
    • mcp: add mcp tool helpers (#1161) (5d58812)

    Chores

    Commits
    • 72d9622 release: 2.32.0
    • b99fa17 feat(api): Add support for cache diagnostics beta
    • 54fefae test: re-enable beta skill/version create tests (#1243)
    • 5224fff chore(api): spec updates
    • dd0d0da codegen metadata
    • 3f9458f feat(api): Add BetaManagedAgentsSearchResultBlock types
    • c43e2f1 ci: pin GitHub Actions to commit SHAs
    • 5228802 feat(mcp): add mcp tool helpers (#1161)
    • 1bc3d65 ci(release-please): don't split out aws release versioning for now (#332)
    • See full diff in compare view

    Updates `org.slf4j:slf4j-simple` from 2.0.17 to 2.0.18 Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Updates `org.slf4j:slf4j-simple` from 2.0.17 to 2.0.18 Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Updates `org.slf4j:slf4j-simple` from 2.0.17 to 2.0.18 Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Updates `org.slf4j:slf4j-simple` from 2.0.17 to 2.0.18 Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Updates `org.slf4j:slf4j-simple` from 2.0.17 to 2.0.18 Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Updates `org.slf4j:slf4j-simple` from 2.0.17 to 2.0.18 Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Updates `org.slf4j:slf4j-simple` from 2.0.17 to 2.0.18 Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Updates `org.slf4j:slf4j-simple` from 2.0.17 to 2.0.18 Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.0 to 9.5.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by upd... _Description has been truncated_ Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 4 ++-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/simple-streamable-server/gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 6d4a97930..f425a5640 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] -anthropic = "2.31.0" +anthropic = "2.32.0" kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.12.0" shadow = "9.4.1" -slf4j = "2.0.17" +slf4j = "2.0.18" [libraries] anthropic-java = { group = "com.anthropic", name = "anthropic-java", version.ref = "anthropic" } diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties index b52fb7e71..df6a6ad76 100644 --- a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index 2eccae2eb..91d7878c3 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.12.0" -slf4j = "2.0.17" +slf4j = "2.0.18" shadow = "9.4.1" [libraries] diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties index b52fb7e71..df6a6ad76 100644 --- a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties index b52fb7e71..df6a6ad76 100644 --- a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index 3296f053d..70a87ca30 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.3.21" ktor = "3.3.3" junit = "6.0.3" mcp-kotlin = "0.12.0" -slf4j = "2.0.17" +slf4j = "2.0.18" shadow = "9.4.1" [libraries] diff --git a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties index b52fb7e71..df6a6ad76 100644 --- a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index 8eee2982e..ac62c36ab 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -5,7 +5,7 @@ ktor = "3.3.3" logging = "8.0.02" mcp-kotlin = "0.12.0" shadow = "9.4.1" -slf4j = "2.0.17" +slf4j = "2.0.18" [libraries] kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "logging" } diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties index b52fb7e71..df6a6ad76 100644 --- a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 From c8545606bb87faf7595dafa7269f31f5a5f75703 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 19:59:31 +0200 Subject: [PATCH 144/187] chore(deps): bump the all-dependencies group across 1 directory with 2 updates (#777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-dependencies group with 2 updates in the /integration-test/src/jvmTest/typescript directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [tsx](https://github.com/privatenumber/tsx). Updates `@types/node` from 25.6.2 to 25.9.0

    Commits

    Updates `tsx` from 4.21.0 to 4.22.1
    Release notes

    Sourced from tsx's releases.

    v4.22.1

    4.22.1 (2026-05-17)

    Bug Fixes

    • resolve tsconfig path aliases containing a colon (#780) (6979f28)

    This release is also available on:

    v4.22.0

    4.22.0 (2026-05-14)

    Features


    This release is also available on:

    v4.21.1

    4.21.1 (2026-05-14)

    Bug Fixes

    • support Node 20.11/21.2 import.meta paths (acf3d8f)
    • support Node.js 24.15.0 (c1d2d45)
    • support Node.js 26.1.0 and 25.9.0 (1d7e528)

    This release is also available on:

    Commits
    • 6979f28 fix: resolve tsconfig path aliases containing a colon (#780)
    • b29f6ee feat: upgrade esbuild to 0.28 (#789)
    • 0dd17e9 test: cover registerHooks loader composition
    • acf3d8f fix: support Node 20.11/21.2 import.meta paths
    • 4bbef80 test: cover configDir paths without baseUrl
    • dddc5ce test: cover sync-hook watch reruns and cleanup retries
    • 09e8f8c test: assert CLI runs without warnings
    • 1d7e528 fix: support Node.js 26.1.0 and 25.9.0
    • c1d2d45 fix: support Node.js 24.15.0
    • d04672d test: update node version feature gates
    • Additional commits viewable in compare view
    Maintainer changes

    This version was pushed to npm by GitHub Actions, a new releaser for tsx since your current version.


    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/jvmTest/typescript/package-lock.json | 264 ++++++++---------- .../src/jvmTest/typescript/package.json | 4 +- 2 files changed, 122 insertions(+), 146 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 6aa468d7f..ac2f59d13 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -15,16 +15,16 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.6.2", + "@types/node": "^25.9.0", "cors": "^2.8.6", - "tsx": "^4.21.0", + "tsx": "^4.22.1", "typescript": "^6.0.3" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -39,9 +39,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -56,9 +56,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -73,9 +73,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -90,9 +90,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -107,9 +107,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -124,9 +124,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -141,9 +141,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -158,9 +158,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -175,9 +175,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -192,9 +192,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -209,9 +209,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -226,9 +226,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -243,9 +243,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -260,9 +260,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -277,9 +277,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -294,9 +294,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -311,9 +311,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ "arm64" ], @@ -328,9 +328,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -345,9 +345,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], @@ -362,9 +362,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -379,9 +379,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", "cpu": [ "arm64" ], @@ -396,9 +396,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -413,9 +413,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -430,9 +430,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -447,9 +447,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -579,13 +579,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", - "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "version": "25.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", + "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/qs": { @@ -888,9 +888,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -901,32 +901,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/escape-html": { @@ -1147,19 +1147,6 @@ "node": ">= 0.4" } }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1508,16 +1495,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1703,14 +1680,13 @@ } }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz", + "integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -1751,9 +1727,9 @@ } }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index 6ec358a83..4e776e965 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -16,9 +16,9 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.6.2", + "@types/node": "^25.9.0", "cors": "^2.8.6", - "tsx": "^4.21.0", + "tsx": "^4.22.1", "typescript": "^6.0.3" } } From 6d5bac1e6792fc42ca476527df19c56f51666885 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 21:09:59 +0200 Subject: [PATCH 145/187] chore(deps): bump gradle-wrapper from 9.5.0 to 9.5.1 (#776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [gradle-wrapper](https://github.com/gradle/gradle) from 9.5.0 to 9.5.1.
    Release notes

    Sourced from gradle-wrapper's releases.

    9.5.1

    The Gradle team is excited to announce Gradle 9.5.1.

    Here are the highlights of this release:

    • Task provenance in reports and failure messages
    • Type-safe accessors for precompiled Kotlin Settings plugins

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: atm1020, mataha, Adam, Attila Kelemen, Benedikt Ritter, Björn Kautler, Caro Silva Rode, CHANHAN, Dmitry Nezavitin, Eng Zer Jun, KugelLibelle, Madalin Valceleanu, Markus Gaisbauer, Oliver Kopp, Philip Wedemann, ploober, Roberto Perez Alcolea, Rohit Anand, Suvrat Acharya, Ujwal Suresh Vanjare, Victor Merkulov

    Upgrade instructions

    Switch your build to use Gradle 9.5.1 by updating your wrapper:

    ./gradlew wrapper --gradle-version=9.5.1 && ./gradlew
    wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • fd78213 Update Documentation Infrastructure: Fix scrolling issue in user manual (#37861)
    • 7758437 fix scroll
    • 2fd605f Only try to run as worker thread in DefaultBuildOperationQueue (#37845)
    • af69849 Release notes for Gradle 9.5.1 (#37853)
    • f4d9d03 Release notes for Gradle 9.5.1
    • 01eda3a Address review feedback on worker-lease retry changes
    • 7024e15 Revert enrich file visitor with size info on release branch (#37848)
    • d51476f Fix tryRunAsWorkerThread null-return test to match contract
    • 090ebab Revert "Add getLength() to FilePropertyVisitor.VisitState"
    • bceab24 Revert "Fix annotation"
    • Additional commits viewable in compare view

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b52fb7e71..df6a6ad76 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 From 1ff5f003b657efef39e41f00591e693afa1757e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 14:53:06 +0200 Subject: [PATCH 146/187] chore(deps): bump qs from 6.14.2 to 6.15.2 in /integration-test/src/jvmTest/typescript (#779) Bumps [qs](https://github.com/ljharb/qs) from 6.14.2 to 6.15.2.
    Changelog

    Sourced from qs's changelog.

    6.15.2

    • [Fix] stringify: skip null/undefined entries in arrayFormat: 'comma' + encodeValuesOnly instead of crashing in encoder
    • [Fix] stringify: use configured delimiter after charsetSentinel (#555)
    • [Fix] stringify: apply formatter to encoded key under strictNullHandling (#554)
    • [Fix] stringify: skip null/undefined filter-array entries instead of crashing in encoder (#551)
    • [Fix] parse: handle nested bracket groups and add regression tests (#530)
    • [readme] fix grammar (#550)
    • [Dev Deps] update @ljharb/eslint-config
    • [Tests] add regression tests for keys containing percent-encoded bracket text

    6.15.1

    • [Fix] parse: parameterLimit: Infinity with throwOnLimitExceeded: true silently drops all parameters
    • [Deps] update @ljharb/eslint-config
    • [Dev Deps] update @ljharb/eslint-config, iconv-lite
    • [Tests] increase coverage

    6.15.0

    • [New] parse: add strictMerge option to wrap object/primitive conflicts in an array (#425, #122)
    • [Fix] duplicates option should not apply to bracket notation keys (#514)
    Commits
    • 9aca407 v6.15.2
    • 5e33d33 [Dev Deps] update @ljharb/eslint-config
    • 21f80b3 [Fix] stringify: skip null/undefined entries in arrayFormat: 'comma' + `e...
    • a0a81ea [Fix] stringify: use configured delimiter after charsetSentinel
    • e3062f7 [Fix] stringify: apply formatter to encoded key under strictNullHandling
    • 0c180a4 [Fix] stringify: skip null/undefined filter-array entries instead of crashi...
    • 3a8b94a [Tests] add regression tests for keys containing percent-encoded bracket text
    • 96755ab [readme] fix grammar
    • a419ce5 [Fix] parse: handle nested bracket groups and add regression tests
    • 3f5e1c5 v6.15.1
    • Additional commits viewable in compare view

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=qs&package-manager=npm_and_yarn&previous-version=6.14.2&new-version=6.15.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/modelcontextprotocol/kotlin-sdk/network/alerts).
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index ac2f59d13..6dbdf45c3 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -1448,9 +1448,9 @@ } }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" From 78066a9cf6970618ef72a8c9b48c8c2fca03c387 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 13:10:59 +0000 Subject: [PATCH 147/187] chore(deps): bump the all-dependencies group across 1 directory with 2 updates (#784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-dependencies group with 2 updates in the /integration-test/src/jvmTest/typescript directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [tsx](https://github.com/privatenumber/tsx). Updates `@types/node` from 25.9.0 to 25.9.1
    Commits

    Updates `tsx` from 4.22.1 to 4.22.3
    Release notes

    Sourced from tsx's releases.

    v4.22.3

    4.22.3 (2026-05-19)

    Bug Fixes

    • decode typed loader source (dce02fc)
    • preserve entrypoint with TypeScript preload hooks (68f72f3)

    This release is also available on:

    v4.22.2

    4.22.2 (2026-05-18)

    Bug Fixes

    • preserve CJS JSON require in ESM hooks (35b700b)
    • preserve named exports from CommonJS TypeScript (11de737)
    • support module.exports require(esm) interop (cf8f199)

    This release is also available on:

    Commits
    • dce02fc fix: decode typed loader source
    • 68f72f3 fix: preserve entrypoint with TypeScript preload hooks
    • 69455cf test: cover package exports for ambiguous ESM reexports
    • 35b700b fix: preserve CJS JSON require in ESM hooks
    • ef807db chore: update testing dependencies
    • 3917090 test: document compatibility test taxonomy
    • de8113f refactor: centralize Node capability facts
    • c1f62db test: consolidate tsconfig path edge coverage
    • 4e08174 test: consolidate loader hook coverage
    • 674bb30 test: consolidate tsImport commonjs mts coverage
    • Additional commits viewable in compare view

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/jvmTest/typescript/package-lock.json | 16 ++++++++-------- .../src/jvmTest/typescript/package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 6dbdf45c3..9e757a5cb 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -15,9 +15,9 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.9.0", + "@types/node": "^25.9.1", "cors": "^2.8.6", - "tsx": "^4.22.1", + "tsx": "^4.22.3", "typescript": "^6.0.3" } }, @@ -579,9 +579,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", - "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "dev": true, "license": "MIT", "dependencies": { @@ -1680,9 +1680,9 @@ } }, "node_modules/tsx": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz", - "integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index 4e776e965..40c58b33f 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -16,9 +16,9 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.9.0", + "@types/node": "^25.9.1", "cors": "^2.8.6", - "tsx": "^4.22.1", + "tsx": "^4.22.3", "typescript": "^6.0.3" } } From ff4b6232ba1a107058f6243463c9f7298b52dd76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 18:27:57 +0200 Subject: [PATCH 148/187] chore(deps): bump the other-dependencies group across 3 directories with 3 updates (#781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-client directory: [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java). Bumps the other-dependencies group with 1 update in the /samples/simple-streamable-server directory: [org.junit.jupiter:junit-jupiter](https://github.com/junit-team/junit-framework). Bumps the other-dependencies group with 1 update in the /samples/weather-stdio-server directory: [io.github.oshai:kotlin-logging-jvm](https://github.com/oshai/kotlin-logging). Updates `com.anthropic:anthropic-java` from 2.32.0 to 2.34.0
    Release notes

    Sourced from com.anthropic:anthropic-java's releases.

    v2.34.0

    2.34.0 (2026-05-21)

    Full Changelog: v2.33.0...v2.34.0

    Features

    • api: Add support for thinking-token-count beta for estimated tokens in thinking block deltas when streaming (c814c7a)

    Bug Fixes

    • test: set required estimatedTokens on BetaThinkingDelta in BetaMessageAccumulatorTest (04aedf3)

    v2.33.0

    2.33.0 (2026-05-19)

    Full Changelog: v2.32.0...v2.33.0

    Features

    • client: Add support for self-hosted sandboxes in CMA with sandbox helpers (d519362)
    Changelog

    Sourced from com.anthropic:anthropic-java's changelog.

    2.34.0 (2026-05-21)

    Full Changelog: v2.33.0...v2.34.0

    Features

    • api: Add support for thinking-token-count beta for estimated tokens in thinking block deltas when streaming (c814c7a)

    Bug Fixes

    • test: set required estimatedTokens on BetaThinkingDelta in BetaMessageAccumulatorTest (04aedf3)

    2.33.0 (2026-05-19)

    Full Changelog: v2.32.0...v2.33.0

    Features

    • client: Add support for self-hosted sandboxes in CMA with sandbox helpers (d519362)
    Commits
    • 765ec0e release: 2.34.0
    • 56c637f fix(test): set required estimatedTokens on BetaThinkingDelta in BetaMessageAc...
    • 5d636b2 feat(api): Add support for thinking-token-count beta for estimated tokens in ...
    • ba2d183 release: 2.33.0
    • 56312db feat(client): Add support for self-hosted sandboxes in CMA with sandbox helpers
    • See full diff in compare view

    Updates `org.junit.jupiter:junit-jupiter` from 6.0.3 to 6.1.0
    Release notes

    Sourced from org.junit.jupiter:junit-jupiter's releases.

    JUnit 6.1.0 = Platform 6.1.0 + Jupiter 6.1.0 + Vintage 6.1.0

    See Release Notes.

    New Contributors

    Full Changelog: https://github.com/junit-team/junit-framework/compare/r6.0.3...r6.1.0

    JUnit 6.1.0-RC1 = Platform 6.1.0-RC1 + Jupiter 6.1.0-RC1 + Vintage 6.1.0-RC1

    See Release Notes.

    New Contributors

    Full Changelog: https://github.com/junit-team/junit-framework/compare/r6.1.0-M1...r6.1.0-RC1

    JUnit 6.1.0-M1 = Platform 6.1.0-M1 + Jupiter 6.1.0-M1 + Vintage 6.1.0-M1

    See Release Notes.

    New Contributors

    Full Changelog: https://github.com/junit-team/junit-framework/compare/r6.0.0...r6.1.0-M1

    Commits

    Updates `io.github.oshai:kotlin-logging-jvm` from 8.0.02 to 8.0.03
    Release notes

    Sourced from io.github.oshai:kotlin-logging-jvm's releases.

    8.0.03

    What's Changed

    Full Changelog: https://github.com/oshai/kotlin-logging/compare/8.0.02...8.0.03

    Commits
    • b641856 bump version to 8.0.04
    • 3e6096d Fix #598: Allow external configuration of logStartupMessage via Properties an...
    • See full diff in compare view

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- samples/simple-streamable-server/gradle/libs.versions.toml | 2 +- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index f425a5640..c263c598c 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -anthropic = "2.32.0" +anthropic = "2.34.0" kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.12.0" diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index 70a87ca30..b607a1cb2 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.3.21" ktor = "3.3.3" -junit = "6.0.3" +junit = "6.1.0" mcp-kotlin = "0.12.0" slf4j = "2.0.18" shadow = "9.4.1" diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index ac62c36ab..daf5fc1dc 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -2,7 +2,7 @@ coroutines = "1.11.0" kotlin = "2.3.21" ktor = "3.3.3" -logging = "8.0.02" +logging = "8.0.03" mcp-kotlin = "0.12.0" shadow = "9.4.1" slf4j = "2.0.18" From 37ee423308078a5daa57bb76953c79f1bcc6c7dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 16:45:06 +0000 Subject: [PATCH 149/187] chore(deps): bump the infrastructure group with 2 updates (#783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the infrastructure group with 2 updates: [io.github.oshai:kotlin-logging](https://github.com/oshai/kotlin-logging) and [io.netty:netty-bom](https://github.com/netty/netty). Updates `io.github.oshai:kotlin-logging` from 8.0.02 to 8.0.03
    Release notes

    Sourced from io.github.oshai:kotlin-logging's releases.

    8.0.03

    What's Changed

    Full Changelog: https://github.com/oshai/kotlin-logging/compare/8.0.02...8.0.03

    Commits
    • b641856 bump version to 8.0.04
    • 3e6096d Fix #598: Allow external configuration of logStartupMessage via Properties an...
    • See full diff in compare view

    Updates `io.netty:netty-bom` from 4.2.13.Final to 4.2.14.Final
    Release notes

    Sourced from io.netty:netty-bom's releases.

    netty-4.2.14.Final

    What's Changed

    New Contributors

    Full Changelog: https://github.com/netty/netty/compare/netty-4.2.13.Final...netty-4.2.14.Final

    Commits
    • 0a60b75 [maven-release-plugin] prepare release netty-4.2.14.Final
    • 72df658 Fix MQTT decoder size check after variable header replay (#16787)
    • 7125dba MQTT: Allow MQTT 5 CONNECT with password only (#16833)
    • 9e19320 IoUring: Stop generic FileRegion drain loop when transferred() reaches count(...
    • 4ce9f17 Route synchronous onLookupComplete exceptions via fireExceptionCaught (#16794)
    • f7b1b7d Fix memoryAddress() for direct ByteBuffers wrapped by Unpooled without Unsafe...
    • 0ccb265 IpFilter: Fix ClassCastException caused by IpSubnetFilter if only ipv6 rules ...
    • a6aeb6d Resolve all localhost addresses without querying DNS servers (#16749)
    • c328ba2 Fix ResumptionController wrapping (#16815)
    • bc5862b HTTP2: Use 100 as default max concurrent streams setting (#16804)
    • Additional commits viewable in compare view

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf36c70e9..54c27c050 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ kover = "0.9.8" ktlint = "14.2.0" knit = "0.5.1" mavenPublish = "0.36.0" -netty = "4.2.13.Final" +netty = "4.2.14.Final" # libraries version awaitility = "4.3.0" @@ -18,7 +18,7 @@ coroutines = "1.11.0" kotest = "6.1.11" kotlinx-io = "0.9.0" ktor = "3.3.3" -logging = "8.0.02" +logging = "8.0.03" mockk = "1.14.9" serialization = "1.11.0" slf4j = "2.0.18" From 0adfb3bcdb06843a9da6617e0a50c604a6747339 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 1 Jun 2026 12:10:01 +0200 Subject: [PATCH 150/187] fix!: harden lifecycle, dispatchers, back-pressure (#778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #573 fixes #574 fixes #708 - Handlers ran on `Dispatchers.IO` → now `Dispatchers.Default`, configurable via `handlerDispatcher`. - Reader/writer dispatcher exposed as `ioDispatcher` (default `IODispatcher.limitedParallelism(2)`). - Optional caller-supplied `CoroutineScope` so the pipeline isn't detached from the caller's tree. - `CancellationException` consistently rethrown across all pumps. - Unbounded channels → bounded with graceful drain on close (in-flight outbound flushed before `onClose`). - Input `Source` now closed on natural EOF / reader error (was leaking for non-`System.in` sources). - Race between `start()` and `close()` fixed via 3-state CAS + `setupComplete` barrier replacing `AtomicBoolean`. ## How Has This Been Tested? unit and integration tests ## Breaking Changes - Handlers default to `Dispatchers.Default` (was `Dispatchers.IO`). Override with `handlerDispatcher = Dispatchers.IO` if your handlers perform blocking I/O. - `send()` suspends under back-pressure instead of buffering unbounded. - `StdioServerTransport(input, output)` ctor is `@Deprecated` (WARNING) in favor of `StdioServerTransport(input, output) { ... }`. Still binary-compatible. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .gitignore | 2 +- kotlin-sdk-server/api/kotlin-sdk-server.api | 11 + .../kotlin/sdk/server/StdioServerTransport.kt | 434 ++++++++++++------ .../sdk/server/StdioServerTransportTest.kt | 342 +++++++++++++- 4 files changed, 642 insertions(+), 147 deletions(-) diff --git a/.gitignore b/.gitignore index d9f0fdec0..805579c50 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,7 @@ node_modules dist ### SWE agents ### -.claude/settings.local.json +.claude/ .junie/ ### Conformance test results ### diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index f400e80d8..3acaa6489 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -219,11 +219,22 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/SseServerTransport 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 (Lkotlinx/io/Source;Lkotlinx/io/Sink;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport$Builder { + public final fun getHandlerDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; + public final fun getIoDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; + public final fun getScope ()Lkotlinx/coroutines/CoroutineScope; + public final fun setHandlerDispatcher (Lkotlinx/coroutines/CoroutineDispatcher;)V + public final fun setIoDispatcher (Lkotlinx/coroutines/CoroutineDispatcher;)V + public final fun setScope (Lkotlinx/coroutines/CoroutineScope;)V +} + public final class io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { public static final field STANDALONE_SSE_STREAM_ID Ljava/lang/String; public fun ()V diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt index a4d588cc5..2b760899a 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt @@ -7,13 +7,23 @@ import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.McpDsl +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.RPCError import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedSendChannelException +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -23,191 +33,345 @@ import kotlinx.io.Source import kotlinx.io.buffered import kotlinx.io.readByteArray import kotlinx.io.writeString -import kotlin.concurrent.atomics.AtomicBoolean +import kotlin.concurrent.atomics.AtomicReference import kotlin.concurrent.atomics.ExperimentalAtomicApi -import kotlin.coroutines.CoroutineContext private const val READ_BUFFER_SIZE = 8192L /** - * A server transport that communicates with a client via standard I/O. + * Server-side MCP transport that exchanges JSON-RPC messages over arbitrary byte streams. * - * Reads from input [Source] and writes to output [Sink]. + * Reads framed messages from the supplied [Source] and writes framed messages to the supplied + * [Sink]. Three internal coroutines drive the pipeline: * - * @constructor Creates a new instance of [StdioServerTransport]. - * @param inputStream The input [Source] used to receive data. - * @param outputStream The output [Sink] used to send data. + * - **reader** — pulls bytes from the input source into the parsing buffer; runs on + * [Builder.ioDispatcher]. + * - **processor** — parses messages out of the buffer and invokes the registered message handler; + * runs on [Builder.handlerDispatcher] (defaults to [Dispatchers.Default]) so blocking handler + * code does not starve the I/O pool. + * - **writer** — serialises outbound messages and flushes them to the output sink; runs on + * [Builder.ioDispatcher]. + * + * Both internal channels are bounded, so a slow handler or slow output naturally back-pressures + * the upstream producer — [send] suspends when the outbound channel is full. + * + * Both explicit [close] and a natural EOF from the input perform a graceful drain: in-flight + * outbound messages are flushed before `onClose` fires, and the input source and output sink are + * released. + * + * Example: + * ```kotlin + * val transport = StdioServerTransport( + * input = System.`in`.asSource().buffered(), + * output = System.out.asSink().buffered(), + * ) { + * scope = myScope + * } + * transport.start() + * ``` */ @OptIn(ExperimentalAtomicApi::class) -public class StdioServerTransport(private val inputStream: Source, outputStream: Sink) : AbstractTransport() { +public class StdioServerTransport private constructor( + private val input: Source, + output: Sink, + private val scope: CoroutineScope?, + private val handlerDispatcher: CoroutineDispatcher, + private val ioDispatcher: CoroutineDispatcher, +) : AbstractTransport() { + + /** + * Creates a [StdioServerTransport] reading from [input] and writing to [output], with + * optional configuration applied through a [Builder] block. + * + * @param input source the transport reads JSON-RPC messages from + * @param output sink the transport writes JSON-RPC messages to + * @param block configuration applied to the underlying [Builder] + */ + public constructor( + input: Source, + output: Sink, + block: Builder.() -> Unit = {}, + ) : this(Builder(input, output).apply(block)) + + private constructor(builder: Builder) : this( + input = builder.input, + output = builder.output, + scope = builder.scope, + handlerDispatcher = builder.handlerDispatcher, + ioDispatcher = builder.ioDispatcher, + ) + + /** + * Creates a [StdioServerTransport] from the given input source and output sink with default + * configuration. Retained for binary compatibility; prefer the [Builder]-based constructor. + * + * @param inputStream source the transport reads JSON-RPC messages from + * @param outputStream sink the transport writes JSON-RPC messages to + */ + @Deprecated( + message = "Use StdioServerTransport(input, output) { ... } instead.", + replaceWith = ReplaceWith("StdioServerTransport(input = inputStream, output = outputStream)"), + level = DeprecationLevel.WARNING, + ) + public constructor(inputStream: Source, outputStream: Sink) : this( + input = inputStream, + output = outputStream, + scope = null, + handlerDispatcher = Dispatchers.Default, + ioDispatcher = IODispatcher.limitedParallelism(2), + ) private val logger = KotlinLogging.logger {} private val readBuffer = ReadBuffer() - private val initialized: AtomicBoolean = AtomicBoolean(false) - private var readingJob: Job? = null - private var sendingJob: Job? = null - private var processingJob: Job? = null + private val readChannel = Channel(Channel.BUFFERED) + private val writeChannel = Channel(Channel.BUFFERED) + private val outputSink = output.buffered() - private val coroutineContext: CoroutineContext = IODispatcher + SupervisorJob() - private val scope = CoroutineScope(coroutineContext) - private val readChannel = Channel(Channel.UNLIMITED) - private val writeChannel = Channel(Channel.UNLIMITED) - private val outputSink = outputStream.buffered() + private enum class State { New, Operational, Stopped } - override suspend fun start() { - if (!initialized.compareAndSet(expectedValue = false, newValue = true)) { - error("StdioServerTransport already started!") - } + private val state: AtomicReference = AtomicReference(State.New) - // Launch a coroutine to read from stdin - readingJob = launchReadingJob() + private var readerJob: Job? = null + private var processorJob: Job? = null + private var writerJob: Job? = null - // Launch a coroutine to process messages from readChannel - processingJob = launchProcessingJob() + private var effectiveScope: CoroutineScope? = null + private val ownsScope: Boolean = (scope == null) - // Launch a coroutine to handle message sending - sendingJob = launchSendingJob() - } + private val setupComplete = CompletableDeferred() - private fun launchReadingJob(): Job { - val job = scope.launch { - val buf = Buffer() - try { - while (isActive) { - val bytesRead = inputStream.readAtMostTo(buf, READ_BUFFER_SIZE) - if (bytesRead == -1L) { - // EOF reached - break - } - if (bytesRead > 0) { - val chunk = buf.readByteArray() - readChannel.send(chunk) - } - } - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - logger.error(e) { "Error reading from stdin" } - _onError.invoke(e) - } finally { - // Reached EOF or error, close connection - close() + /** + * Starts the reader, processor, and writer coroutines. Must be called exactly once before + * messages can be exchanged; subsequent calls throw. + */ + override suspend fun start() { + if (!state.compareAndSet(State.New, State.Operational)) { + when (state.load()) { + State.Stopped -> error("StdioServerTransport is already closed!") + else -> error("StdioServerTransport already started!") } } - job.invokeOnCompletion { cause -> - logJobCompletion("Message reading", cause) - } - return job - } - private fun launchProcessingJob(): Job { - val job = scope.launch { - try { - for (chunk in readChannel) { - readBuffer.append(chunk) - processReadBuffer() - } - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - _onError.invoke(e) - } - } - job.invokeOnCompletion { cause -> - logJobCompletion("Processing", cause) + try { + val resolvedScope = scope ?: CoroutineScope( + currentCoroutineContext() + IODispatcher + SupervisorJob(), + ) + effectiveScope = resolvedScope + + readerJob = resolvedScope.launch( + ioDispatcher + CoroutineName("StdioServerTransport.reader"), + ) { readerPump() } + processorJob = resolvedScope.launch( + handlerDispatcher + CoroutineName("StdioServerTransport.processor"), + start = CoroutineStart.UNDISPATCHED, + ) { processorPump() } + writerJob = resolvedScope.launch( + ioDispatcher + CoroutineName("StdioServerTransport.writer"), + start = CoroutineStart.UNDISPATCHED, + ) { writerPump() } + } finally { + setupComplete.complete(Unit) } - return job } - private fun launchSendingJob(): Job { - val job = scope.launch { - try { - for (message in writeChannel) { - val json = serializeMessage(message) - outputSink.writeString(json) - outputSink.flush() + private suspend fun readerPump() { + val buf = Buffer() + try { + while (currentCoroutineContext().isActive) { + val bytesRead = input.readAtMostTo(buf, READ_BUFFER_SIZE) + if (bytesRead == -1L) break + if (bytesRead > 0) { + val chunk = buf.readByteArray() + readChannel.send(chunk) } - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - logger.error(e) { "Error writing to stdout" } - _onError.invoke(e) } - } - job.invokeOnCompletion { cause -> - logJobCompletion("Message sending", cause) - if (cause is CancellationException) { - readingJob?.cancel(cause) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + if (state.load() == State.Stopped) { + logger.debug(e) { "Reader interrupted by close()" } + } else { + logger.error(e) { "Error reading from input source" } + _onError(e) } + } finally { + readChannel.close() } - return job } - private suspend fun processReadBuffer() { - while (true) { - val message = try { - readBuffer.readMessage() - } catch (e: Throwable) { - _onError.invoke(e) - null - } - - if (message == null) break - // Async invocation broke delivery order - try { - _onMessage.invoke(message) - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - logger.error(e) { "Error processing message" } - _onError.invoke(e) + private suspend fun processorPump() { + try { + for (chunk in readChannel) { + readBuffer.append(chunk) + while (true) { + val message = try { + readBuffer.readMessage() + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + _onError(e) + null + } ?: break + try { + _onMessage(message) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.error(e) { "Error processing message" } + _onError(e) + } + } } + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + _onError(e) + } finally { + writeChannel.close() } } - private fun logJobCompletion(jobName: String, cause: Throwable?) { - when (cause) { - is CancellationException -> { - } - - null -> { - logger.debug { "$jobName job completed" } - } - - else -> { - logger.debug(cause) { "$jobName job completed exceptionally" } + private suspend fun writerPump() { + try { + for (message in writeChannel) { + val json = serializeMessage(message) + outputSink.writeString(json) + outputSink.flush() } + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.error(e) { "Error writing to output sink" } + _onError(e) + } finally { + transitionToStoppedNaturally() } } + /** + * Closes the transport. When called after [start], waits for in-flight outbound messages to be + * flushed, releases the input source and output sink, cancels the internal scope when the + * transport owns it, and invokes `onClose`. When called before [start], transitions directly + * to the closed state without releasing the input/output or invoking `onClose` — the caller + * remains responsible for resources that were never handed off to a running transport. Safe to + * call multiple times and safe to race with [start]. + */ override suspend fun close() { - if (!initialized.compareAndSet(expectedValue = true, newValue = false)) return + var previous: State + do { + previous = state.load() + } while (previous != State.Stopped && !state.compareAndSet(previous, State.Stopped)) + + if (previous == State.New) { + setupComplete.complete(Unit) + return + } withContext(NonCancellable) { - writeChannel.close() - sendingJob?.cancelAndJoin() + setupComplete.await() - runCatching { - inputStream.close() - }.onFailure { logger.warn(it) { "Failed to close stdin" } } + if (previous == State.Stopped) { + writerJob?.join() + return@withContext + } - readingJob?.cancel() + runCatching { input.close() } + .onFailure { logger.warn(it) { "Failed to close input source" } } + readerJob?.cancel() readChannel.close() + processorJob?.cancelAndJoin() + writeChannel.close() + writerJob?.join() + runCatching { outputSink.close() } + .onFailure { logger.warn(it) { "Failed to close output sink" } } + readBuffer.clear() + if (ownsScope) { + effectiveScope?.coroutineContext?.get(Job)?.cancel() + } + invokeOnCloseCallback() + } + } - processingJob?.cancelAndJoin() + /** + * Queues [message] for the writer coroutine. Suspends when the outbound channel is full, + * applying back-pressure to the caller. Throws [McpException] with + * [RPCError.ErrorCode.CONNECTION_CLOSED] if the transport has not been started or has + * already closed. + * + * @param message JSON-RPC message to send + * @param options transport-specific send options; ignored by this transport + */ + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + when (state.load()) { + State.New -> throw McpException( + code = RPCError.ErrorCode.CONNECTION_CLOSED, + message = "Transport is not started", + ) - readBuffer.clear() + State.Stopped -> throw McpException( + code = RPCError.ErrorCode.CONNECTION_CLOSED, + message = "Transport is closed", + ) - runCatching { - outputSink.flush() - outputSink.close() - }.onFailure { logger.warn(it) { "Failed to close stdout" } } + State.Operational -> Unit + } + try { + writeChannel.send(message) + } catch (e: CancellationException) { + throw e + } catch (e: ClosedSendChannelException) { + throw McpException( + code = RPCError.ErrorCode.CONNECTION_CLOSED, + message = "Transport is closed", + cause = e, + ) + } + } - invokeOnCloseCallback() + private fun transitionToStoppedNaturally() { + if (!state.compareAndSet(State.Operational, State.Stopped)) return + runCatching { input.close() } + .onFailure { logger.warn(it) { "Failed to close input source" } } + runCatching { outputSink.close() } + .onFailure { logger.warn(it) { "Failed to close output sink" } } + readBuffer.clear() + if (ownsScope) { + // Non-null in practice: writer launches after effectiveScope is published. + effectiveScope?.coroutineContext?.get(Job)?.cancel() } + invokeOnCloseCallback() } - override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { - writeChannel.send(message) + /** + * Configuration builder for [StdioServerTransport]. Used via the + * `StdioServerTransport(input, output) { ... }` factory; the I/O endpoints are supplied + * positionally, while [scope], [handlerDispatcher], and [ioDispatcher] are configurable + * inside the block. + * + * Example: + * ```kotlin + * StdioServerTransport(input, output) { + * scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + * handlerDispatcher = myHandlerDispatcher + * ioDispatcher = Dispatchers.IO + * } + * ``` + * + * @property scope Optional caller-supplied [CoroutineScope]. When non-null, the transport's + * pipeline coroutines run as children of this scope and the transport does not tear it + * down on [close]. When `null`, the transport creates and owns an internal scope. + * @property handlerDispatcher Dispatcher for invoking the registered message handler. + * Defaults to [Dispatchers.Default]. + * @property ioDispatcher Dispatcher for the reader and writer coroutines. Must allow at + * least two threads to run concurrently so the reader and writer don't block each other. + * The default is a two-thread view of the platform I/O dispatcher + * (`IODispatcher.limitedParallelism(2)`); pass a different value to share or isolate I/O + * threads with the rest of your application. + */ + @McpDsl + public class Builder internal constructor(internal val input: Source, internal val output: Sink) { + public var scope: CoroutineScope? = null + public var handlerDispatcher: CoroutineDispatcher = Dispatchers.Default + public var ioDispatcher: CoroutineDispatcher = IODispatcher.limitedParallelism(2) } } diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt index 32123a693..c6370a1a6 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt @@ -5,17 +5,29 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.withClue import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.McpException import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.RPCError import io.modelcontextprotocol.kotlin.sdk.types.toJSON import io.modelcontextprotocol.kotlin.test.utils.runIntegrationTest import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull import kotlinx.io.Buffer import kotlinx.io.RawSink import kotlinx.io.RawSource @@ -33,9 +45,12 @@ import java.io.ByteArrayOutputStream import java.io.IOException import java.io.PipedInputStream import java.io.PipedOutputStream +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors import kotlin.test.assertFalse import kotlin.test.assertTrue import kotlin.test.fail +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -71,13 +86,13 @@ class StdioServerTransportTest { @Test fun `should be safe to close before start`() = runIntegrationTest { - val server = StdioServerTransport(bufferedInput, printOutput) + val server = StdioServerTransport(input = bufferedInput, output = printOutput) server.close() // initialized guard makes this a no-op; must not throw } @Test fun `should start then close cleanly`() = runIntegrationTest { - val server = StdioServerTransport(bufferedInput, printOutput) + val server = StdioServerTransport(input = bufferedInput, output = printOutput) server.onError { error -> throw error } @@ -96,7 +111,7 @@ class StdioServerTransportTest { @Test fun `should not read until started`() = runIntegrationTest { - val server = StdioServerTransport(bufferedInput, printOutput) + val server = StdioServerTransport(input = bufferedInput, output = printOutput) server.onError { error -> throw error } @@ -125,7 +140,7 @@ class StdioServerTransportTest { @Test fun `should read multiple messages`() = runIntegrationTest { - val server = StdioServerTransport(bufferedInput, printOutput) + val server = StdioServerTransport(input = bufferedInput, output = printOutput) server.onError { error -> throw error } @@ -162,7 +177,7 @@ class StdioServerTransportTest { @ParameterizedTest(name = "[{index}] input throws {0}") @MethodSource("inputErrors") fun `should invoke onError when input stream throws`(throwable: Throwable): Unit = runIntegrationTest { - val server = StdioServerTransport(FaultyRawSource(throwable).buffered(), printOutput) + val server = StdioServerTransport(input = FaultyRawSource(throwable).buffered(), output = printOutput) val capturedError = CompletableDeferred() server.onError { capturedError.complete(it) } server.onMessage {} @@ -176,7 +191,7 @@ class StdioServerTransportTest { @ParameterizedTest(name = "[{index}] output throws {0}") @MethodSource("outputErrors") fun `should invoke onError when output sink throws`(throwable: Throwable): Unit = runIntegrationTest { - val server = StdioServerTransport(bufferedInput, FaultyRawSink(throwable).buffered()) + val server = StdioServerTransport(input = bufferedInput, output = FaultyRawSink(throwable).buffered()) val capturedError = CompletableDeferred() server.onError { capturedError.complete(it) } server.onMessage {} @@ -190,7 +205,7 @@ class StdioServerTransportTest { @Test fun `should call onClose when input EOF is reached`(): Unit = runIntegrationTest { - val server = StdioServerTransport(bufferedInput, printOutput) + val server = StdioServerTransport(input = bufferedInput, output = printOutput) val didClose = CompletableDeferred() server.onError { throw it } server.onClose { didClose.complete(Unit) } @@ -206,7 +221,7 @@ class StdioServerTransportTest { @Test fun `should throw when starting twice`(): Unit = runIntegrationTest { - val server = StdioServerTransport(bufferedInput, printOutput) + val server = StdioServerTransport(input = bufferedInput, output = printOutput) server.onMessage {} server.start() withClue("Server should not start twice") { @@ -217,10 +232,24 @@ class StdioServerTransportTest { server.close() } + @Test + fun `should report 'closed' error message when starting after close`(): Unit = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + server.onError {} + server.onMessage {} + server.start() + server.close() + + val ex = shouldThrow { + server.start() + } + ex.message shouldContain "closed" + } + @ParameterizedTest(name = "[{index}] handler throws {0}") @MethodSource("handlerErrors") fun `should continue processing messages after handler throws`(throwable: Throwable) = runIntegrationTest { - val server = StdioServerTransport(bufferedInput, printOutput) + val server = StdioServerTransport(input = bufferedInput, output = printOutput) val capturedErrors = mutableListOf() val receivedMessages = mutableListOf() val secondMessageProcessed = CompletableDeferred() @@ -253,7 +282,7 @@ class StdioServerTransportTest { @Test fun `should not invoke onError for CancellationException in handler`() = runIntegrationTest { - val server = StdioServerTransport(bufferedInput, printOutput) + val server = StdioServerTransport(input = bufferedInput, output = printOutput) val capturedError = CompletableDeferred() server.onError { capturedError.complete(it) } @@ -281,7 +310,7 @@ class StdioServerTransportTest { @Test fun `should continue receiving valid messages after malformed JSON is skipped`() = runIntegrationTest { - val server = StdioServerTransport(bufferedInput, printOutput) + val server = StdioServerTransport(input = bufferedInput, output = printOutput) val received = CompletableDeferred() // ReadBuffer silently skips unparseable lines — no onError callback expected server.onError {} @@ -299,6 +328,297 @@ class StdioServerTransportTest { server.close() } + @Test + fun `should throw McpException when send is called before start`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val ex = shouldThrow { + server.send(PingRequest().toJSON()) + } + ex.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + } + + @Test + fun `should throw McpException when send is called after close`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + server.onError {} + server.onMessage {} + server.start() + server.close() + + val ex = shouldThrow { + server.send(PingRequest().toJSON()) + } + ex.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + } + + @Test + fun `should drain in-flight outbound messages on graceful close`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + server.onError {} + server.onMessage {} + server.start() + + val numMessages = 50 + repeat(numMessages) { + server.send(PingRequest().toJSON()) + } + server.close() + + val outputLines = String(output.toByteArray()).lines().count { it.isNotBlank() } + outputLines shouldBe numMessages + } + + @Test + fun `should fail-fast when reader throws non-EOF IOException`() = runIntegrationTest { + val ioError = IOException("transient stream failure") + val server = StdioServerTransport(input = FaultyRawSource(ioError).buffered(), output = printOutput) + val errorCaptured = CompletableDeferred() + val closeCaptured = CompletableDeferred() + server.onError { errorCaptured.complete(it) } + server.onClose { closeCaptured.complete(Unit) } + server.onMessage {} + + server.start() + + errorCaptured.await() shouldBe ioError + closeCaptured.await() + } + + @Test + fun `should close input source on natural EOF`() = runIntegrationTest { + val inputClosed = CompletableDeferred() + val eofSource = object : RawSource { + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long = -1L + override fun close() { + inputClosed.complete(Unit) + } + } + val server = StdioServerTransport(input = eofSource.buffered(), output = printOutput) + val onCloseFired = CompletableDeferred() + server.onError {} + server.onMessage {} + server.onClose { onCloseFired.complete(Unit) } + + server.start() + + inputClosed.await() + onCloseFired.await() + } + + // region: concurrency + + @Test + fun `should be safe under concurrent start and close`() = runIntegrationTest(timeout = 30.seconds) { + val iterations = 2000 + repeat(iterations) { iteration -> + val server = StdioServerTransport(input = Buffer(), output = Buffer()) + server.onError {} + server.onMessage {} + + coroutineScope { + launch(Dispatchers.Default) { + try { + server.start() + } catch (_: IllegalStateException) { + } + } + launch(Dispatchers.Default) { + server.close() + } + } + + withClue("iteration $iteration: send() after race should fail with CONNECTION_CLOSED") { + val ex = shouldThrow { + server.send(PingRequest().toJSON()) + } + ex.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + } + } + } + + @Test + fun `should suspend send under back-pressure`() = runIntegrationTest(timeout = 30.seconds) { + val unblock = CountDownLatch(1) + val blockingSink = object : RawSink { + override fun write(source: Buffer, byteCount: Long) { + source.clear() + unblock.await() + } + + override fun flush() { + // noop + } + + override fun close() { + unblock.countDown() + } + } + val server = StdioServerTransport(input = bufferedInput, output = blockingSink.buffered()) + server.onError {} + server.onMessage {} + server.start() + + val maxAttempts = 256 + var suspendedAt = -1 + for (i in 0 until maxAttempts) { + val ok = withTimeoutOrNull(100.milliseconds) { server.send(PingRequest().toJSON()) } + if (ok == null) { + suspendedAt = i + break + } + } + assertTrue( + suspendedAt in 0 until maxAttempts, + "send() should have suspended within $maxAttempts attempts, got suspendedAt=$suspendedAt", + ) + // Unblock writer so close() can drain. + unblock.countDown() + server.close() + } + + @Test + fun `should not hang close when handler suspends indefinitely`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + val handlerEntered = CompletableDeferred() + val neverComplete = CompletableDeferred() + server.onError {} + server.onMessage { + handlerEntered.complete(Unit) + neverComplete.await() + } + server.start() + + inputWriter.write(serializeMessage(PingRequest().toJSON())) + inputWriter.flush() + + handlerEntered.await() + server.close() + } + + @Test + fun `should not hang close when onMessage was never registered`() = runIntegrationTest { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) + server.onError {} + server.start() + + inputWriter.write(serializeMessage(PingRequest().toJSON())) + inputWriter.flush() + + delay(200.milliseconds) + + server.close() + } + + // endregion + + // region: scope and dispatcher knobs + + @Test + fun `should honor externally provided CoroutineScope`() = runIntegrationTest { + val externalScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + val server = StdioServerTransport(input = bufferedInput, output = printOutput) { + scope = externalScope + } + val closeCaptured = CompletableDeferred() + server.onError {} + server.onClose { closeCaptured.complete(Unit) } + server.onMessage {} + + server.start() + externalScope.cancel() + + closeCaptured.await() + + val ex = shouldThrow { + server.send(PingRequest().toJSON()) + } + ex.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + } + + @Test + fun `should use ioDispatcher for reader and writer`() = runIntegrationTest { + val threadName = "stdio-test-io-thread" + val executor = Executors.newFixedThreadPool(2) { r -> Thread(r, threadName) } + try { + val readerThread = CompletableDeferred() + val writerThread = CompletableDeferred() + val unblockReader = CountDownLatch(1) + val recordingInput = object : RawSource { + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long { + if (!readerThread.isCompleted) { + readerThread.complete(Thread.currentThread().name) + } + unblockReader.await() + return -1L + } + + override fun close() { + unblockReader.countDown() + } + } + val recordingOutput = object : RawSink { + override fun write(source: Buffer, byteCount: Long) { + if (!writerThread.isCompleted) { + writerThread.complete(Thread.currentThread().name) + } + source.clear() + } + + override fun flush() { + // noop + } + + override fun close() { + // noop + } + } + val server = StdioServerTransport( + input = recordingInput.buffered(), + output = recordingOutput.buffered(), + ) { + ioDispatcher = executor.asCoroutineDispatcher() + } + server.onError {} + server.onMessage {} + server.start() + + readerThread.await() shouldContain threadName + server.send(PingRequest().toJSON()) + writerThread.await() shouldContain threadName + + server.close() + } finally { + executor.shutdownNow() + } + } + + @Test + fun `should use handlerDispatcher for message handling`() = runIntegrationTest { + val threadName = "stdio-test-handler-thread" + val executor = Executors.newSingleThreadExecutor { r -> Thread(r, threadName) } + try { + val server = StdioServerTransport(input = bufferedInput, output = printOutput) { + handlerDispatcher = executor.asCoroutineDispatcher() + } + val observedThreadName = CompletableDeferred() + server.onError {} + server.onMessage { + observedThreadName.complete(Thread.currentThread().name) + } + server.start() + + inputWriter.write(serializeMessage(PingRequest().toJSON())) + inputWriter.flush() + + observedThreadName.await() shouldContain threadName + server.close() + } finally { + executor.shutdownNow() + } + } + + // endregion + private fun inputErrors() = listOf( IOException("simulated read failure"), RuntimeException("unexpected read exception"), From 0eb18228538c5410957e73933509fca2c00902c1 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 1 Jun 2026 12:33:39 +0200 Subject: [PATCH 151/187] Update Ktor to version 3.4.3 (#785) ktor 3.3.3 -> 3.4.3 --- 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 54c27c050..4982b6d37 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ collections-immutable = "0.4.0" coroutines = "1.11.0" kotest = "6.1.11" kotlinx-io = "0.9.0" -ktor = "3.3.3" +ktor = "3.4.3" logging = "8.0.03" mockk = "1.14.9" serialization = "1.11.0" From 6e6f80512fb8fcc9f3c031cfd693ccbcf9c4aaab Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 2 Jun 2026 12:31:28 +0200 Subject: [PATCH 152/187] Merge commit from fork --- kotlin-sdk-core/api/kotlin-sdk-core.api | 6 +++ .../kotlin/sdk/shared/ReadBuffer.kt | 40 ++++++++++++++++++- .../kotlin/sdk/shared/ReadBufferTest.kt | 33 +++++++++++++++ .../kotlin/sdk/server/StdioServerTransport.kt | 3 ++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index b588eef2a..089572293 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -125,6 +125,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestOptions : io public fun toString ()Ljava/lang/String; } +public final class io/modelcontextprotocol/kotlin/sdk/shared/TooLongFrameException : java/io/IOException { + public fun (JI)V + public final fun getFrameSize ()J + public final fun getMaxFrameSize ()I +} + public abstract interface class io/modelcontextprotocol/kotlin/sdk/shared/Transport { public abstract fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun onClose (Lkotlin/jvm/functions/Function0;)V diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt index c0c853f62..8102cf60f 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt @@ -4,13 +4,24 @@ import io.github.oshai.kotlinlogging.KotlinLogging import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.McpJson import kotlinx.io.Buffer +import kotlinx.io.IOException import kotlinx.io.indexOf import kotlinx.io.readString /** * Buffers a continuous stdio stream into discrete JSON-RPC messages. + * + * A single newline-terminated frame may not exceed an internal size cap (16 MiB): a peer that streams + * bytes without ever sending a newline triggers a [TooLongFrameException] from [readMessage] instead + * of growing the buffer without bound. A non-positive cap disables the check. */ -public class ReadBuffer { +public class ReadBuffer internal constructor(private val maxFrameSize: Int) { + + public constructor() : this(DEFAULT_MAX_FRAME_SIZE) + + internal companion object { + internal const val DEFAULT_MAX_FRAME_SIZE: Int = 16 * 1024 * 1024 + } private val logger = KotlinLogging.logger { } @@ -53,7 +64,11 @@ public class ReadBuffer { private fun readNextLine(): String? { val lfIndex = if (buffer.exhausted()) -1L else buffer.indexOf('\n'.code.toByte()) - if (lfIndex == -1L) return null + if (lfIndex == -1L) { + failIfFrameTooLong(buffer.size) + return null + } + failIfFrameTooLong(lfIndex) return if (lfIndex == 0L) { buffer.skip(1) @@ -71,6 +86,13 @@ public class ReadBuffer { } } + private fun failIfFrameTooLong(frameSize: Long) { + if (maxFrameSize in 1..(line) /** Serializes a [JSONRPCMessage] to its JSON string representation with a trailing newline. */ diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt index 7b4f05308..e5e50f801 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt @@ -6,7 +6,9 @@ import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification import kotlinx.serialization.json.Json import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNull class ReadBufferTest { @@ -75,4 +77,35 @@ class ReadBufferTest { val message = readBuffer.readMessage() assertEquals(testMessage, message) } + + @Test + fun `should fail when an unframed blob exceeds the cap`() { + val readBuffer = ReadBuffer(maxFrameSize = 64) + // No newline ever arrives: the memory-exhaustion vector. + readBuffer.append(ByteArray(100) { 'a'.code.toByte() }) + val ex = assertFailsWith { readBuffer.readMessage() } + assertContains(ex.message.orEmpty(), "maximum size") + } + + @Test + fun `should fail when a completed line exceeds the cap`() { + val readBuffer = ReadBuffer(maxFrameSize = 64) + readBuffer.append(ByteArray(100) { 'a'.code.toByte() } + '\n'.code.toByte()) + assertFailsWith { readBuffer.readMessage() } + } + + @Test + fun `should not enforce a cap when maxFrameSize is non-positive`() { + val readBuffer = ReadBuffer(maxFrameSize = 0) + // Well beyond any small cap and still no newline — must not throw when disabled. + readBuffer.append(ByteArray(8192) { 'a'.code.toByte() }) + assertNull(readBuffer.readMessage()) + } + + @Test + fun `should parse a message that fits under the cap`() { + val readBuffer = ReadBuffer(maxFrameSize = 1024) + readBuffer.append(serializeMessage(testMessage).encodeToByteArray()) + assertEquals(testMessage, readBuffer.readMessage()) + } } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt index 2b760899a..9261be257 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt @@ -4,6 +4,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging import io.modelcontextprotocol.kotlin.sdk.internal.IODispatcher import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer +import io.modelcontextprotocol.kotlin.sdk.shared.TooLongFrameException import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage @@ -208,6 +209,8 @@ public class StdioServerTransport private constructor( readBuffer.readMessage() } catch (e: CancellationException) { throw e + } catch (e: TooLongFrameException) { + throw e } catch (e: Throwable) { _onError(e) null From b7eeeb12058099f980fa98d1468506489a6b5f32 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 2 Jun 2026 13:30:40 +0200 Subject: [PATCH 153/187] release: 0.13.0 (#796) bump version to 0.13.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e145506f6..e5294e812 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,4 +8,4 @@ kotlin.daemon.jvmargs=-Xmx4G kotlin.native.ignoreDisabledTargets=true group=io.modelcontextprotocol -version=0.12.0 +version=0.13.0 From 59b44a30e4ab816bc4658cfab3be69b90bf76063 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:59:56 +0200 Subject: [PATCH 154/187] chore(deps): bump the other-dependencies group across 5 directories with 4 updates (#789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the other-dependencies group with 2 updates in the /samples/kotlin-mcp-client directory: [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java) and [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 2 updates in the /samples/kotlinlang-mcp-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow) and [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback). Bumps the other-dependencies group with 1 update in the /samples/simple-streamable-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 2 updates in the /samples/weather-stdio-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow) and [io.github.oshai:kotlin-logging-jvm](https://github.com/oshai/kotlin-logging). Updates `com.anthropic:anthropic-java` from 2.34.0 to 2.35.0
    Release notes

    Sourced from com.anthropic:anthropic-java's releases.

    v2.35.0

    2.35.0 (2026-05-28)

    Full Changelog: v2.34.1...v2.35.0

    Features

    • api: Add support for claude-opus-4-8, mid-conversation system blocks, and usage.output_tokens_details (3e4621e)

    v2.34.1

    2.34.1 (2026-05-27)

    Full Changelog: v2.34.0...v2.34.1

    Bug Fixes

    • structured-outputs: support all nullability annotations (#1254) (95e4391)

    Documentation

    • replace literal newlines (c5573d9)
    Changelog

    Sourced from com.anthropic:anthropic-java's changelog.

    2.35.0 (2026-05-28)

    Full Changelog: v2.34.1...v2.35.0

    Features

    • api: Add support for claude-opus-4-8, mid-conversation system blocks, and usage.output_tokens_details (3e4621e)

    2.34.1 (2026-05-27)

    Full Changelog: v2.34.0...v2.34.1

    Bug Fixes

    • structured-outputs: support all nullability annotations (#1254) (95e4391)

    Documentation

    • replace literal newlines (c5573d9)
    Commits
    • 65d26cc release: 2.35.0
    • f901010 feat(api): Add support for claude-opus-4-8, mid-conversation system blocks, a...
    • bc1bdd9 release: 2.34.1
    • f27e209 fix(structured-outputs): support all nullability annotations (#1254)
    • 62ef906 docs: replace literal newlines
    • dc19d08 ci: run breaking change detection on every branch (#1224)
    • 1382286 codegen metadata
    • See full diff in compare view

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `ch.qos.logback:logback-classic` from 1.5.32 to 1.5.34
    Release notes

    Sourced from ch.qos.logback:logback-classic's releases.

    Logback 1.5.34

    2026-06-01 Release of logback version 1.5.34

    • In case certain StackTraceElement values returned by the Throwable.getStackTrace method are null, StackTraceElementProxy substitutes a dummy instance instead of throwing an IllegalArgumentException. This resolves [issues #1040](qos-ch/logback#1040), reported by Naotsugu Kobayashi.

    • HardenedObjectInputStream will now throw an InvalidClassException during deserialization attempts of Proxy classes. This change addresses potential deserialization whitelist bypass vulnerability reported by York Shen and registered as CVE-2026-10532.

    • A bitwise identical binary of this version can be reproduced by building from source code at commit e62272ac152469aec1ede056c3c7d0d7314e7bfe associated with the tag v_1.5.34. This release was built using Java "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian 11.6.

    Logback 1.5.33

    2026-05-27 Release of logback version 1.5.33

    • PropertiesConfiguratorModelHandler now registers properties file URLs to the ConfigurationWatchList when scan is enabled (via local scan="true" attribute or top-level configuration scan), ensuring changes are detected and reconfiguration occurs. This problem was reported in issues/1034.

    • When processing <conversionRule> elements and both class and converterClass attributes are specified, silently use the class attribute without issuing a warning. However, if the attribute values differ, a warning will be issued. This change was requested in issues/1031.

    • HardenedModelInputStream will no longer accept to deserialize all classes located under the "java.lang" and "java.util" packages but a limited number of explicitly authorized classes in those packages. This potential deserialization whitelist bypass vulnerability was reported by York Shen and registered as CVE-2026-9828.

    • SSL parameters for SSLSocketAppender now enable hostname verification by default. Moreover, the default protocol is now "TLSv1.2". This potential vulnerability was reported by York Shen.

    • When printing the status message field, ViewStatusMessagesServletBase now escapes special characters such as "&" as character entities. This potential vulnerability was reported by York Shen.

    • A bit-wise identical binary of this version can be reproduced by building from source code at commit 124e8b49b55ac34d08743a0646bd463410192647 associated with the tag v_1.5.33. Release built using Java "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian 11.6.

    Commits
    • e62272a prepare release 1.5.34
    • 1e9e926 add resolveProxyClassRejectsDynamicProxies unit test
    • 2de5cbe added StackTraceElementProxyTest, minor edits to AGENTS.md
    • 0e9b927 in case StackTraceElement is null use a substitute, fixing issues/1040
    • f7a0654 prevent resolveProxyClass bypass
    • 249b81f docs are no longer distributed
    • 1c3b26a start work on 1.5.34-SNAPSHOT
    • 124e8b4 prepare release 1.5.33
    • d8fd6f2 escapeTags in message field when printing status messages
    • 95edbeb hostnameVerification default to true in SSLParametersConfiguration, SSL.DEFAU...
    • Additional commits viewable in compare view

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Updates `io.github.oshai:kotlin-logging-jvm` from 8.0.03 to 8.0.4
    Release notes

    Sourced from io.github.oshai:kotlin-logging-jvm's releases.

    8.0.4

    Align to semver.

    Full Changelog: https://github.com/oshai/kotlin-logging/compare/8.0.03...8.0.4

    Commits

    Updates `com.gradleup.shadow` from 9.4.1 to 9.4.2
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.2

    Changed

    • Update jdependency to support Java 27. (#2033)
    Commits

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 4 ++-- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- samples/kotlinlang-mcp-server/gradle/libs.versions.toml | 4 ++-- samples/simple-streamable-server/gradle/libs.versions.toml | 2 +- samples/weather-stdio-server/gradle/libs.versions.toml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index c263c598c..42d700e38 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -anthropic = "2.34.0" +anthropic = "2.35.0" kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.12.0" -shadow = "9.4.1" +shadow = "9.4.2" slf4j = "2.0.18" [libraries] diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index 91d7878c3..165d09616 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.12.0" slf4j = "2.0.18" -shadow = "9.4.1" +shadow = "9.4.2" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml index 5b7988e24..9a68e310a 100644 --- a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -4,8 +4,8 @@ ktor = "3.3.3" mcp-kotlin = "0.12.0" serialization = "1.11.0" coroutines = "1.11.0" -logback = "1.5.32" -shadow = "9.4.1" +logback = "1.5.34" +shadow = "9.4.2" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index b607a1cb2..07bdac1d0 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -4,7 +4,7 @@ ktor = "3.3.3" junit = "6.1.0" mcp-kotlin = "0.12.0" slf4j = "2.0.18" -shadow = "9.4.1" +shadow = "9.4.2" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index daf5fc1dc..d7a6fcbff 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -2,9 +2,9 @@ coroutines = "1.11.0" kotlin = "2.3.21" ktor = "3.3.3" -logging = "8.0.03" +logging = "8.0.4" mcp-kotlin = "0.12.0" -shadow = "9.4.1" +shadow = "9.4.2" slf4j = "2.0.18" [libraries] From 32cc44115d601ae0dea1dde6e6dd487ce6a7d355 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:48:50 +0200 Subject: [PATCH 155/187] chore(deps): bump org.jetbrains.kotlinx:atomicfu-gradle-plugin from 0.32.1 to 0.33.0 in the kotlinx group across 1 directory (#791) Bumps the kotlinx group with 1 update in the / directory: [org.jetbrains.kotlinx:atomicfu-gradle-plugin](https://github.com/Kotlin/kotlinx.atomicfu). Updates `org.jetbrains.kotlinx:atomicfu-gradle-plugin` from 0.32.1 to 0.33.0
    Release notes

    Sourced from org.jetbrains.kotlinx:atomicfu-gradle-plugin's releases.

    0.33.0

    • Deprecated atomic array types provided by the atomicfu. Use types provided by the Kotlin standard library instead. (#591)
    • Published jar files now include Implementation-* attributes in their manifests (#592)
    • Updated ASM to 9.10.1 (#595)
    • Various build infra updates
    Changelog

    Sourced from org.jetbrains.kotlinx:atomicfu-gradle-plugin's changelog.

    Versoin 0.33.0

    • Deprecated atomic array types provided by the atomicfu. Use types provided by the Kotlin standard library instead. (#591)
    • Published jar files now include Implementation-* attributes in their manifests (#592)
    • Updated ASM to 9.10.1 (#595)
    • Various build infra updates
    Commits
    • fc175fc Version 0.33.0
    • dc6187a Update dependencies (#595)
    • 8e23c32 Deprecate AFU array types (#591)
    • 0d27eb1 Do not use deprecated js(compiler: KotlinJsCompilerType, ...) DSL (#594)
    • 6c6b2d6 Fill Implementation-* attributes in JAR's manifests (#592)
    • a658e7d KT-84955: add a suppressor for gradle plugin error checker for native target ...
    • 1dd25ee Add logic to handle changed .kotlin_module file name in test (#581)
    • See full diff in compare view

    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 4982b6d37..e8fbe8d6c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] # plugins version -atomicfu = "0.32.1" +atomicfu = "0.33.0" binaryCompatibilityValidatorPlugin = "0.18.1" detekt = "2.0.0-alpha.3" dokka = "2.2.0" From 4b726c8f5f250735ead11052ff89dbf63d82a7c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:49:40 +0200 Subject: [PATCH 156/187] chore(deps): bump io.mockk:mockk from 1.14.9 to 1.14.11 in the testing group across 1 directory (#792) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the testing group with 1 update in the / directory: [io.mockk:mockk](https://github.com/mockk/mockk). Updates `io.mockk:mockk` from 1.14.9 to 1.14.11
    Release notes

    Sourced from io.mockk:mockk's releases.

    v1.14.11

    What's Changed

    New Contributors

    Full Changelog: https://github.com/mockk/mockk/compare/1.14.9...v1.14.11

    Commits
    • d5617fc Version bump for v1.14.11
    • d4e6a00 Merge pull request #1530 from hspedro/fix/726-openj9-classcast-message-format
    • 98e91bc fix: handle OpenJ9 CCE message in JvmAutoHinter
    • 945f746 Merge pull request #1529 from snowykte0426/fix/1273-jvmtiagent-agp85-native-l...
    • 35cc073 [ 1273-jvmtiagent ] chore: stop tracking .claude/scheduled_tasks.lock
    • d6aa82b [ 1273-jvmtiagent ] chore: drop useLegacyPackaging workaround
    • 85a907c [ 1273-jvmtiagent ] chore: apply spotless formatting
    • 7d9aec1 [ 1273-jvmtiagent ] fix: extract .so when findLibrary returns a path with '='
    • 1d5d30e [ 1273-jvmtiagent ] test: verify findLibrary resolves mockkjvmtiagent to a va...
    • 21a6a3a [ 1273-jvmtiagent]fix: pass findLibrary path directly to attach JvmtiAgent
    • Additional commits viewable in compare view

    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 e8fbe8d6c..83e7a6f78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ kotest = "6.1.11" kotlinx-io = "0.9.0" ktor = "3.4.3" logging = "8.0.03" -mockk = "1.14.9" +mockk = "1.14.11" serialization = "1.11.0" slf4j = "2.0.18" logback = "1.5.32" From cf88353b35216998a6d379b2ef10daeccb47de82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:39:32 +0200 Subject: [PATCH 157/187] chore(deps): bump ch.qos.logback:logback-classic from 1.5.32 to 1.5.34 (#794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.32 to 1.5.34.
    Release notes

    Sourced from ch.qos.logback:logback-classic's releases.

    Logback 1.5.34

    2026-06-01 Release of logback version 1.5.34

    • In case certain StackTraceElement values returned by the Throwable.getStackTrace method are null, StackTraceElementProxy substitutes a dummy instance instead of throwing an IllegalArgumentException. This resolves [issues #1040](qos-ch/logback#1040), reported by Naotsugu Kobayashi.

    • HardenedObjectInputStream will now throw an InvalidClassException during deserialization attempts of Proxy classes. This change addresses potential deserialization whitelist bypass vulnerability reported by York Shen and registered as CVE-2026-10532.

    • A bitwise identical binary of this version can be reproduced by building from source code at commit e62272ac152469aec1ede056c3c7d0d7314e7bfe associated with the tag v_1.5.34. This release was built using Java "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian 11.6.

    Logback 1.5.33

    2026-05-27 Release of logback version 1.5.33

    • PropertiesConfiguratorModelHandler now registers properties file URLs to the ConfigurationWatchList when scan is enabled (via local scan="true" attribute or top-level configuration scan), ensuring changes are detected and reconfiguration occurs. This problem was reported in issues/1034.

    • When processing <conversionRule> elements and both class and converterClass attributes are specified, silently use the class attribute without issuing a warning. However, if the attribute values differ, a warning will be issued. This change was requested in issues/1031.

    • HardenedModelInputStream will no longer accept to deserialize all classes located under the "java.lang" and "java.util" packages but a limited number of explicitly authorized classes in those packages. This potential deserialization whitelist bypass vulnerability was reported by York Shen and registered as CVE-2026-9828.

    • SSL parameters for SSLSocketAppender now enable hostname verification by default. Moreover, the default protocol is now "TLSv1.2". This potential vulnerability was reported by York Shen.

    • When printing the status message field, ViewStatusMessagesServletBase now escapes special characters such as "&" as character entities. This potential vulnerability was reported by York Shen.

    • A bit-wise identical binary of this version can be reproduced by building from source code at commit 124e8b49b55ac34d08743a0646bd463410192647 associated with the tag v_1.5.33. Release built using Java "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian 11.6.

    Commits
    • e62272a prepare release 1.5.34
    • 1e9e926 add resolveProxyClassRejectsDynamicProxies unit test
    • 2de5cbe added StackTraceElementProxyTest, minor edits to AGENTS.md
    • 0e9b927 in case StackTraceElement is null use a substitute, fixing issues/1040
    • f7a0654 prevent resolveProxyClass bypass
    • 249b81f docs are no longer distributed
    • 1c3b26a start work on 1.5.34-SNAPSHOT
    • 124e8b4 prepare release 1.5.33
    • d8fd6f2 escapeTags in message field when printing status messages
    • 95edbeb hostnameVerification default to true in SSLParametersConfiguration, SSL.DEFAU...
    • Additional commits viewable in compare view

    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 83e7a6f78..84746803b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ logging = "8.0.03" mockk = "1.14.11" serialization = "1.11.0" slf4j = "2.0.18" -logback = "1.5.32" +logback = "1.5.34" junit = "5.14.4" [libraries] From 2f71156483845752f4678287e466810eff468ea6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:07:54 +0200 Subject: [PATCH 158/187] chore(deps): bump tsx from 4.22.3 to 4.22.4 in /integration-test/src/jvmTest/typescript in the all-dependencies group across 1 directory (#795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-dependencies group with 1 update in the /integration-test/src/jvmTest/typescript directory: [tsx](https://github.com/privatenumber/tsx). Updates `tsx` from 4.22.3 to 4.22.4
    Release notes

    Sourced from tsx's releases.

    v4.22.4

    4.22.4 (2026-05-31)

    Bug Fixes

    • resolve CommonJS directory requires inside dependencies (#803) (1ce8463)

    This release is also available on:

    Commits

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 8 ++++---- integration-test/src/jvmTest/typescript/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 9e757a5cb..6e5fb5fae 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -17,7 +17,7 @@ "@types/express": "^5.0.6", "@types/node": "^25.9.1", "cors": "^2.8.6", - "tsx": "^4.22.3", + "tsx": "^4.22.4", "typescript": "^6.0.3" } }, @@ -1680,9 +1680,9 @@ } }, "node_modules/tsx": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", - "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index 40c58b33f..0fafdb92c 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -18,7 +18,7 @@ "@types/express": "^5.0.6", "@types/node": "^25.9.1", "cors": "^2.8.6", - "tsx": "^4.22.3", + "tsx": "^4.22.4", "typescript": "^6.0.3" } } From 52f8b0fa919c59dcae068e85b8e337f532704128 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 01:30:10 +0200 Subject: [PATCH 159/187] chore(deps): bump the infrastructure group across 1 directory with 2 updates (#797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the infrastructure group with 2 updates in the / directory: [io.github.oshai:kotlin-logging](https://github.com/oshai/kotlin-logging) and [io.netty:netty-bom](https://github.com/netty/netty). Updates `io.github.oshai:kotlin-logging` from 8.0.03 to 8.0.4
    Release notes

    Sourced from io.github.oshai:kotlin-logging's releases.

    8.0.4

    Align to semver.

    Full Changelog: https://github.com/oshai/kotlin-logging/compare/8.0.03...8.0.4

    Commits

    Updates `io.netty:netty-bom` from 4.2.14.Final to 4.2.15.Final
    Release notes

    Sourced from io.netty:netty-bom's releases.

    netty-4.2.15.Final

    Security fixes

    • CVE-2026-48059: memory exhaustion in io.netty:netty-codec-haproxy (high).
    • CVE-2026-47691: DNS cache poisoning in io.netty:netty-resolver-dns (high).
    • CVE-2026-XXXXX: DDoS in io.netty:netty-codec-http2.
    • CVE-2026-XXXXX: memory exhaustion in io.netty:netty-codec-redis (high).
    • CVE-2026-44250: memory exhaustion in io.netty:netty-codec-redis (high).
    • CVE-2026-44890: memory exhaustion in io.netty:netty-codec-redis (high).
    • CVE-2026-XXXXX: information disclosure and denial of service in io.netty:netty-codec-classes-quic.
    • CVE-2026-44249: IPv6 subnet filter bypass in io.netty:netty-handler (high).
    • CVE-2026-XXXXX: request smuggling in io.netty:netty-codec-http.
    • CVE-2026-44892: memory exhaustion in io.netty:netty-codec-http3 (high).
    • CVE-2026-44893: memory leak in io.netty:netty-codec-haproxy (high).
    • CVE-2026-44894: traffic amplification in io.netty:netty-codec-classes-quic (high).
    • CVE-2026-XXXXX: TLS hostname verification accidentally disabled in io.netty:netty-handler (high).
    • CVE-2026-45673: DNS cache poisoning in io.netty:netty-resolver-dns.
    • CVE-2026-45416: excessive memory usage from SNIHandler in io.netty:netty-handler (high).
    • CVE-2026-45536: file descriptor leak in io.netty:netty-transport-native-epoll and io.netty:netty-transport-native-kqueue.
    • CVE-2026-45674: DNS cache poisoning in io.netty:netty-resolver-dns (high).
    • CVE-2026-46340: memory exhaustion in io.netty:netty-transport-sctp (high).
    • CVE-2026-47244: denial of service in io.netty:netty-codec-http2.
    • CVE-2026-48006: memory exhaustion in io.netty:netty-codec-redis (high).
    • CVE-2026-48748: memory exhaustion in io.netty:netty-codec-http3 (high).
    • CVE-2026-48043: memory exhaustion in io.netty:netty-codec-http2.

    What's Changed

    New Contributors

    Full Changelog: https://github.com/netty/netty/compare/netty-4.2.14.Final...netty-4.2.15.Final

    Commits
    • a41f7b2 [maven-release-plugin] prepare release netty-4.2.15.Final
    • 2394530 Auto-port 4.2: MQTT: Reject malformed no-payload packets with non-zero Remain...
    • 0bd1657 Add maxWindowLog parameter to ZstdDecoder to bound memory allocation (#16850)
    • 76291f5 Fix SCTP and Redis tests (#16893)
    • e067b6e Fix revapi warnings (#16885)
    • 5a52600 Pass maxAllocation to Brotli and Zstd decoders (#16844)
    • 541add0 Merge commit from fork
    • 270800e Merge commit from fork
    • 3d45a1e Merge commit from fork
    • 75127ca Merge commit from fork
    • Additional commits viewable in compare view

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 84746803b..4904ea4b3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ kover = "0.9.8" ktlint = "14.2.0" knit = "0.5.1" mavenPublish = "0.36.0" -netty = "4.2.14.Final" +netty = "4.2.15.Final" # libraries version awaitility = "4.3.0" @@ -18,7 +18,7 @@ coroutines = "1.11.0" kotest = "6.1.11" kotlinx-io = "0.9.0" ktor = "3.4.3" -logging = "8.0.03" +logging = "8.0.4" mockk = "1.14.11" serialization = "1.11.0" slf4j = "2.0.18" From a1f11fae2e8a5150360fec4dccbd34b6686fcaec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 05:31:56 +0200 Subject: [PATCH 160/187] chore(deps): bump hono from 4.12.18 to 4.12.23 in /integration-test/src/jvmTest/typescript (#799) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [hono](https://github.com/honojs/hono) from 4.12.18 to 4.12.23.
    Release notes

    Sourced from hono's releases.

    v4.12.23

    What's Changed

    Full Changelog: https://github.com/honojs/hono/compare/v4.12.22...v4.12.23

    v4.12.22

    What's Changed

    New Contributors

    Full Changelog: https://github.com/honojs/hono/compare/v4.12.21...v4.12.22

    v4.12.21

    Security fixes

    This release includes fixes for the following security issues:

    app.mount() strips mount prefix using undecoded path, causing incorrect routing for percent-encoded paths

    Affects: app.mount(). Fixes prefix stripping using the raw URL pathname instead of the decoded path, where percent-encoded characters in the mount prefix or path could cause the prefix to be removed at the wrong position, resulting in the sub-application receiving an incorrect path. GHSA-2gcr-mfcq-wcc3

    IP Restriction bypasses static deny rules for non-canonical IPv6

    Affects: hono/ip-restriction. Fixes IP address comparison using string equality, where non-canonical IPv6 representations of a denied address — such as compressed forms or hex-notation IPv4-mapped addresses — could bypass static deny rules. GHSA-xrhx-7g5j-rcj5

    Cookie helper does not sanitize sameSite and priority, allowing Set-Cookie injection

    Affects: hono/cookie. Fixes missing validation of sameSite and priority options against injection characters (;, \r, \n), where user-controlled input passed to either option could inject additional attributes into the Set-Cookie response header. GHSA-3hrh-pfw6-9m5x

    JWT middleware accepts any Authorization scheme, not only Bearer

    Affects: hono/jwt, hono/jwk. Fixes missing scheme validation in the Authorization header, where any two-part header value was accepted regardless of the scheme name, allowing non-Bearer schemes to pass JWT authentication. GHSA-f577-qrjj-4474


    Users who use app.mount(), hono/ip-restriction, hono/cookie, or hono/jwt/hono/jwk are encouraged to upgrade to this version.

    ... (truncated)

    Commits
    • 83bfb3b 4.12.23
    • bcd290a fix(utils/ipaddr): do not compress a single 0 group to :: (#4971)
    • c968177 feat(compress): add contentTypeFilter option and `COMPRESSIBLE_CONTENT_TYPE_R...
    • 0265a54 docs(contribution): add AI Usage Policy (#4970)
    • c84c5d2 feat(context): export the Context class publicly (#4543)
    • 82dad62 fix(serve-static): normalize all backslashes in file paths, not just the firs...
    • 2f01b77 4.12.22
    • 6bc0dff feat: add msgpack as a compressible content type (#4957)
    • 7e0555d fix(deno): echo negotiated WebSocket subprotocol in upgrade response (#4955)
    • f0ed246 fix(compress): respect Accept-Encoding when encoding option is set (#4951)
    • Additional commits viewable in compare view

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=hono&package-manager=npm_and_yarn&previous-version=4.12.18&new-version=4.12.23)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/modelcontextprotocol/kotlin-sdk/network/alerts).
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 6e5fb5fae..ed07e4ea3 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -1184,9 +1184,9 @@ } }, "node_modules/hono": { - "version": "4.12.18", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", - "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "license": "MIT", "engines": { "node": ">=16.9.0" From 75920e1eb789ad183ea51c8dc338db638207a59a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:39:50 +0200 Subject: [PATCH 161/187] chore(deps): bump mcp-kotlin from 0.12.0 to 0.13.0 in /samples/kotlinlang-mcp-server (#824) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.12.0 to 0.13.0. Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.12.0 to 0.13.0
    Release notes

    Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

    0.13.0

    Description

    Adds a tasks capability and a typed elicitation capability, hardens transport lifecycle, dispatchers, and back-pressure, and enables DNS rebinding protection by default.

    Breaking Changes

    Typed elicitation capability and new tasks capability by @​devcrocod in #732

    ClientCapabilities.elicitation is now a typed ClientCapabilities.Elicitation (form, url) instead of a raw JsonObject?, and both ClientCapabilities and ServerCapabilities gained a typed tasks capability. The constructors of both types changed and are binary-incompatible; recompilation is required.

    Transport handlers now run on Dispatchers.Default by @​devcrocod in #778

    Handlers moved from Dispatchers.IO to Dispatchers.Default (override via handlerDispatcher), send() now suspends under back-pressure instead of buffering unbounded, and the two-argument StdioServerTransport(input, output) constructor is deprecated in favor of the builder-lambda form. Also fixes a start()/close() race, consistent CancellationException propagation, and an input-Source leak (closes #573, #574, #708).

    DNS rebinding protection enabled by default by @​devcrocod in #659

    enableDnsRebindingProtection now defaults to true on mcpStreamableHttp/mcpStatelessStreamableHttp, and the Route.mcp() / Application.mcp() overloads gained enableDnsRebindingProtection, allowedHosts, and allowedOrigins parameters (binary-incompatible on JVM). Only localhost, 127.0.0.1, and [::1] are allowed by default; opt out with enableDnsRebindingProtection = false. The Configuration.enableDnsRebindingProtection, .allowedHosts, and .allowedOrigins properties are deprecated in favor of the new DnsRebindingProtection plugin.

    Features

    • Add a tasks capability (list, cancel, and per-request augmentation for sampling and elicitation) on ClientCapabilities and ServerCapabilities, with assertions for tasks/* and notifications/tasks/status enforced by Client and ServerSession by @​devcrocod in #732
    • Configurable handlerDispatcher and ioDispatcher, plus an optional caller-supplied CoroutineScope, on transports by @​devcrocod in #778

    Fixed

    • Fire onClose callbacks on SseClientTransport when the SSE session ends — whether closed gracefully or terminated by an error — matching the other client transports by @​jskjw157 in #738

    Security

    • Enable DNS rebinding protection by default across all HTTP transports, including the SSE endpoints that previously had none (see Breaking Changes for migration) by @​devcrocod in #659
    • Cap a single stdio JSON-RPC frame at 16 MiB, throwing the new TooLongFrameException instead of buffering unbounded when a peer never sends a newline terminator by @​devcrocod in 6e6f805

    Maintenance

    • Remove the test-only dev.mokksy:mokksy dependency in favor of MockEngine and embeddedServer by @​devcrocod in #754

    Dependencies

    • Ktor to v3.4.3 in #785
    • kotlinx.coroutines to v1.11.0 in #760
    • kotlin-logging to v8.0.03 in #752, #783
    • 24 further Dependabot updates across sample projects, TypeScript conformance-test fixtures, and build/test tooling (JUnit, slf4j-simple, Gradle wrapper, anthropic-java)

    Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.12.0...0.13.0

    Commits
    • b7eeeb1 release: 0.13.0 (#796)
    • 6e6f805 Merge commit from fork
    • 0eb1822 Update Ktor to version 3.4.3 (#785)
    • 0adfb3b fix!: harden lifecycle, dispatchers, back-pressure (#778)
    • 37ee423 chore(deps): bump the infrastructure group with 2 updates (#783)
    • ff4b623 chore(deps): bump the other-dependencies group across 3 directories with 3 up...
    • 78066a9 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • 1ff5f00 chore(deps): bump qs from 6.14.2 to 6.15.2 in /integration-test/src/jvmTest/t...
    • 6d5bac1 chore(deps): bump gradle-wrapper from 9.5.0 to 9.5.1 (#776)
    • c854560 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • Additional commits viewable in compare view

    Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.12.0 to 0.13.0
    Release notes

    Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

    0.13.0

    Description

    Adds a tasks capability and a typed elicitation capability, hardens transport lifecycle, dispatchers, and back-pressure, and enables DNS rebinding protection by default.

    Breaking Changes

    Typed elicitation capability and new tasks capability by @​devcrocod in #732

    ClientCapabilities.elicitation is now a typed ClientCapabilities.Elicitation (form, url) instead of a raw JsonObject?, and both ClientCapabilities and ServerCapabilities gained a typed tasks capability. The constructors of both types changed and are binary-incompatible; recompilation is required.

    Transport handlers now run on Dispatchers.Default by @​devcrocod in #778

    Handlers moved from Dispatchers.IO to Dispatchers.Default (override via handlerDispatcher), send() now suspends under back-pressure instead of buffering unbounded, and the two-argument StdioServerTransport(input, output) constructor is deprecated in favor of the builder-lambda form. Also fixes a start()/close() race, consistent CancellationException propagation, and an input-Source leak (closes #573, #574, #708).

    DNS rebinding protection enabled by default by @​devcrocod in #659

    enableDnsRebindingProtection now defaults to true on mcpStreamableHttp/mcpStatelessStreamableHttp, and the Route.mcp() / Application.mcp() overloads gained enableDnsRebindingProtection, allowedHosts, and allowedOrigins parameters (binary-incompatible on JVM). Only localhost, 127.0.0.1, and [::1] are allowed by default; opt out with enableDnsRebindingProtection = false. The Configuration.enableDnsRebindingProtection, .allowedHosts, and .allowedOrigins properties are deprecated in favor of the new DnsRebindingProtection plugin.

    Features

    • Add a tasks capability (list, cancel, and per-request augmentation for sampling and elicitation) on ClientCapabilities and ServerCapabilities, with assertions for tasks/* and notifications/tasks/status enforced by Client and ServerSession by @​devcrocod in #732
    • Configurable handlerDispatcher and ioDispatcher, plus an optional caller-supplied CoroutineScope, on transports by @​devcrocod in #778

    Fixed

    • Fire onClose callbacks on SseClientTransport when the SSE session ends — whether closed gracefully or terminated by an error — matching the other client transports by @​jskjw157 in #738

    Security

    • Enable DNS rebinding protection by default across all HTTP transports, including the SSE endpoints that previously had none (see Breaking Changes for migration) by @​devcrocod in #659
    • Cap a single stdio JSON-RPC frame at 16 MiB, throwing the new TooLongFrameException instead of buffering unbounded when a peer never sends a newline terminator by @​devcrocod in 6e6f805

    Maintenance

    • Remove the test-only dev.mokksy:mokksy dependency in favor of MockEngine and embeddedServer by @​devcrocod in #754

    Dependencies

    • Ktor to v3.4.3 in #785
    • kotlinx.coroutines to v1.11.0 in #760
    • kotlin-logging to v8.0.03 in #752, #783
    • 24 further Dependabot updates across sample projects, TypeScript conformance-test fixtures, and build/test tooling (JUnit, slf4j-simple, Gradle wrapper, anthropic-java)

    Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.12.0...0.13.0

    Commits
    • b7eeeb1 release: 0.13.0 (#796)
    • 6e6f805 Merge commit from fork
    • 0eb1822 Update Ktor to version 3.4.3 (#785)
    • 0adfb3b fix!: harden lifecycle, dispatchers, back-pressure (#778)
    • 37ee423 chore(deps): bump the infrastructure group with 2 updates (#783)
    • ff4b623 chore(deps): bump the other-dependencies group across 3 directories with 3 up...
    • 78066a9 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • 1ff5f00 chore(deps): bump qs from 6.14.2 to 6.15.2 in /integration-test/src/jvmTest/t...
    • 6d5bac1 chore(deps): bump gradle-wrapper from 9.5.0 to 9.5.1 (#776)
    • c854560 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • Additional commits viewable in compare view

    Updates `io.modelcontextprotocol:kotlin-sdk-testing` from 0.12.0 to 0.13.0
    Release notes

    Sourced from io.modelcontextprotocol:kotlin-sdk-testing's releases.

    0.13.0

    Description

    Adds a tasks capability and a typed elicitation capability, hardens transport lifecycle, dispatchers, and back-pressure, and enables DNS rebinding protection by default.

    Breaking Changes

    Typed elicitation capability and new tasks capability by @​devcrocod in #732

    ClientCapabilities.elicitation is now a typed ClientCapabilities.Elicitation (form, url) instead of a raw JsonObject?, and both ClientCapabilities and ServerCapabilities gained a typed tasks capability. The constructors of both types changed and are binary-incompatible; recompilation is required.

    Transport handlers now run on Dispatchers.Default by @​devcrocod in #778

    Handlers moved from Dispatchers.IO to Dispatchers.Default (override via handlerDispatcher), send() now suspends under back-pressure instead of buffering unbounded, and the two-argument StdioServerTransport(input, output) constructor is deprecated in favor of the builder-lambda form. Also fixes a start()/close() race, consistent CancellationException propagation, and an input-Source leak (closes #573, #574, #708).

    DNS rebinding protection enabled by default by @​devcrocod in #659

    enableDnsRebindingProtection now defaults to true on mcpStreamableHttp/mcpStatelessStreamableHttp, and the Route.mcp() / Application.mcp() overloads gained enableDnsRebindingProtection, allowedHosts, and allowedOrigins parameters (binary-incompatible on JVM). Only localhost, 127.0.0.1, and [::1] are allowed by default; opt out with enableDnsRebindingProtection = false. The Configuration.enableDnsRebindingProtection, .allowedHosts, and .allowedOrigins properties are deprecated in favor of the new DnsRebindingProtection plugin.

    Features

    • Add a tasks capability (list, cancel, and per-request augmentation for sampling and elicitation) on ClientCapabilities and ServerCapabilities, with assertions for tasks/* and notifications/tasks/status enforced by Client and ServerSession by @​devcrocod in #732
    • Configurable handlerDispatcher and ioDispatcher, plus an optional caller-supplied CoroutineScope, on transports by @​devcrocod in #778

    Fixed

    • Fire onClose callbacks on SseClientTransport when the SSE session ends — whether closed gracefully or terminated by an error — matching the other client transports by @​jskjw157 in #738

    Security

    • Enable DNS rebinding protection by default across all HTTP transports, including the SSE endpoints that previously had none (see Breaking Changes for migration) by @​devcrocod in #659
    • Cap a single stdio JSON-RPC frame at 16 MiB, throwing the new TooLongFrameException instead of buffering unbounded when a peer never sends a newline terminator by @​devcrocod in 6e6f805

    Maintenance

    • Remove the test-only dev.mokksy:mokksy dependency in favor of MockEngine and embeddedServer by @​devcrocod in #754

    Dependencies

    • Ktor to v3.4.3 in #785
    • kotlinx.coroutines to v1.11.0 in #760
    • kotlin-logging to v8.0.03 in #752, #783
    • 24 further Dependabot updates across sample projects, TypeScript conformance-test fixtures, and build/test tooling (JUnit, slf4j-simple, Gradle wrapper, anthropic-java)

    Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.12.0...0.13.0

    Commits
    • b7eeeb1 release: 0.13.0 (#796)
    • 6e6f805 Merge commit from fork
    • 0eb1822 Update Ktor to version 3.4.3 (#785)
    • 0adfb3b fix!: harden lifecycle, dispatchers, back-pressure (#778)
    • 37ee423 chore(deps): bump the infrastructure group with 2 updates (#783)
    • ff4b623 chore(deps): bump the other-dependencies group across 3 directories with 3 up...
    • 78066a9 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • 1ff5f00 chore(deps): bump qs from 6.14.2 to 6.15.2 in /integration-test/src/jvmTest/t...
    • 6d5bac1 chore(deps): bump gradle-wrapper from 9.5.0 to 9.5.1 (#776)
    • c854560 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • Additional commits viewable in compare view

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlinlang-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml index 9a68e310a..fdef6d30b 100644 --- a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.3.21" ktor = "3.3.3" -mcp-kotlin = "0.12.0" +mcp-kotlin = "0.13.0" serialization = "1.11.0" coroutines = "1.11.0" logback = "1.5.34" From fd8afcf27fbebb26daef87fdf0328b0aa0159185 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:14:31 +0200 Subject: [PATCH 162/187] chore(deps): bump com.anthropic:anthropic-java from 2.35.0 to 2.40.1 in /samples/kotlin-mcp-client in the other-dependencies group across 1 directory (#818) Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-client directory: [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java). Updates `com.anthropic:anthropic-java` from 2.35.0 to 2.40.1
    Release notes

    Sourced from com.anthropic:anthropic-java's releases.

    v2.40.1

    2.40.1 (2026-06-09)

    Full Changelog: v2.40.0...v2.40.1

    Bug Fixes

    • api: add frontier_llm refusal category (1627fd3)

    v2.40.0

    2.40.0 (2026-06-09)

    Full Changelog: v2.39.0...v2.40.0

    Features

    • api: add support for Managed Agents deployments and environment variable credentials (628720b)

    v2.39.0

    2.39.0 (2026-06-09)

    Full Changelog: v2.38.0...v2.39.0

    Features

    • api: add support for claude-mythos-5 and claude-fable-5, with support for server-side fallbacks on refusal (835fb42)
    • client: adds client-side fallbacks middleware for API providers that do not support server-side fallbacks (835fb42)

    Bug Fixes

    • 3p middleware ordering (#34) (d01da3a)
    • streaming: merge message_delta usage without dropping or rejecting iterations (#39) (e3af118)

    v2.38.0

    2.38.0 (2026-06-06)

    Full Changelog: v2.37.0...v2.38.0

    Features

    • api: small updates to Managed Agents types (c7b08d8)

    v2.37.0

    2.37.0 (2026-06-05)

    Full Changelog: v2.36.0...v2.37.0

    Features

    ... (truncated)

    Changelog

    Sourced from com.anthropic:anthropic-java's changelog.

    2.40.1 (2026-06-09)

    Full Changelog: v2.40.0...v2.40.1

    Bug Fixes

    • api: add frontier_llm refusal category (1627fd3)

    2.40.0 (2026-06-09)

    Full Changelog: v2.39.0...v2.40.0

    Features

    • api: add support for Managed Agents deployments and environment variable credentials (628720b)

    2.39.0 (2026-06-09)

    Full Changelog: v2.38.0...v2.39.0

    Features

    • api: add support for claude-mythos-5 and claude-fable-5, with support for server-side fallbacks on refusal (835fb42)
    • client: adds client-side fallbacks middleware for API providers that do not support server-side fallbacks (835fb42)

    Bug Fixes

    • 3p middleware ordering (#34) (d01da3a)
    • streaming: merge message_delta usage without dropping or rejecting iterations (#39) (e3af118)

    2.38.0 (2026-06-06)

    Full Changelog: v2.37.0...v2.38.0

    Features

    • api: small updates to Managed Agents types (c7b08d8)

    2.37.0 (2026-06-05)

    Full Changelog: v2.36.0...v2.37.0

    Features

    • api: mark Claude Opus 4.1 as deprecated (693cbb4)

    2.36.0 (2026-06-04)

    Full Changelog: v2.35.0...v2.36.0

    ... (truncated)

    Commits
    • 43b8020 release: 2.40.1 (#350)
    • 8f3e69b release: 2.40.0
    • 1540132 feat(api): add support for Managed Agents deployments and environment variabl...
    • a214f31 release: 2.39.0
    • 194e2f4 feat(api): add support for claude-mythos-5 and claude-fable-5, with support f...
    • 56a2a49 fix(streaming): merge message_delta usage without dropping or rejecting itera...
    • a8c6d0f fix: 3p middleware ordering (#34)
    • 79e8568 release: 2.38.0
    • e7c8e6e feat(api): small updates to Managed Agents types
    • edc1ae0 release: 2.37.0
    • Additional commits viewable in compare view

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 42d700e38..2b5eb4987 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -anthropic = "2.35.0" +anthropic = "2.40.1" kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.12.0" From 0851f367972699fc3c588c0c8cabe7023cc03485 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:37:03 +0200 Subject: [PATCH 163/187] chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.12.0 to 0.13.0 in /samples/kotlin-mcp-client (#820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [io.modelcontextprotocol:kotlin-sdk-client](https://github.com/modelcontextprotocol/kotlin-sdk) from 0.12.0 to 0.13.0.
    Release notes

    Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

    0.13.0

    Description

    Adds a tasks capability and a typed elicitation capability, hardens transport lifecycle, dispatchers, and back-pressure, and enables DNS rebinding protection by default.

    Breaking Changes

    Typed elicitation capability and new tasks capability by @​devcrocod in #732

    ClientCapabilities.elicitation is now a typed ClientCapabilities.Elicitation (form, url) instead of a raw JsonObject?, and both ClientCapabilities and ServerCapabilities gained a typed tasks capability. The constructors of both types changed and are binary-incompatible; recompilation is required.

    Transport handlers now run on Dispatchers.Default by @​devcrocod in #778

    Handlers moved from Dispatchers.IO to Dispatchers.Default (override via handlerDispatcher), send() now suspends under back-pressure instead of buffering unbounded, and the two-argument StdioServerTransport(input, output) constructor is deprecated in favor of the builder-lambda form. Also fixes a start()/close() race, consistent CancellationException propagation, and an input-Source leak (closes #573, #574, #708).

    DNS rebinding protection enabled by default by @​devcrocod in #659

    enableDnsRebindingProtection now defaults to true on mcpStreamableHttp/mcpStatelessStreamableHttp, and the Route.mcp() / Application.mcp() overloads gained enableDnsRebindingProtection, allowedHosts, and allowedOrigins parameters (binary-incompatible on JVM). Only localhost, 127.0.0.1, and [::1] are allowed by default; opt out with enableDnsRebindingProtection = false. The Configuration.enableDnsRebindingProtection, .allowedHosts, and .allowedOrigins properties are deprecated in favor of the new DnsRebindingProtection plugin.

    Features

    • Add a tasks capability (list, cancel, and per-request augmentation for sampling and elicitation) on ClientCapabilities and ServerCapabilities, with assertions for tasks/* and notifications/tasks/status enforced by Client and ServerSession by @​devcrocod in #732
    • Configurable handlerDispatcher and ioDispatcher, plus an optional caller-supplied CoroutineScope, on transports by @​devcrocod in #778

    Fixed

    • Fire onClose callbacks on SseClientTransport when the SSE session ends — whether closed gracefully or terminated by an error — matching the other client transports by @​jskjw157 in #738

    Security

    • Enable DNS rebinding protection by default across all HTTP transports, including the SSE endpoints that previously had none (see Breaking Changes for migration) by @​devcrocod in #659
    • Cap a single stdio JSON-RPC frame at 16 MiB, throwing the new TooLongFrameException instead of buffering unbounded when a peer never sends a newline terminator by @​devcrocod in 6e6f805

    Maintenance

    • Remove the test-only dev.mokksy:mokksy dependency in favor of MockEngine and embeddedServer by @​devcrocod in #754

    Dependencies

    • Ktor to v3.4.3 in #785
    • kotlinx.coroutines to v1.11.0 in #760
    • kotlin-logging to v8.0.03 in #752, #783
    • 24 further Dependabot updates across sample projects, TypeScript conformance-test fixtures, and build/test tooling (JUnit, slf4j-simple, Gradle wrapper, anthropic-java)

    Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.12.0...0.13.0

    Commits
    • b7eeeb1 release: 0.13.0 (#796)
    • 6e6f805 Merge commit from fork
    • 0eb1822 Update Ktor to version 3.4.3 (#785)
    • 0adfb3b fix!: harden lifecycle, dispatchers, back-pressure (#778)
    • 37ee423 chore(deps): bump the infrastructure group with 2 updates (#783)
    • ff4b623 chore(deps): bump the other-dependencies group across 3 directories with 3 up...
    • 78066a9 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • 1ff5f00 chore(deps): bump qs from 6.14.2 to 6.15.2 in /integration-test/src/jvmTest/t...
    • 6d5bac1 chore(deps): bump gradle-wrapper from 9.5.0 to 9.5.1 (#776)
    • c854560 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • Additional commits viewable in compare view

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 2b5eb4987..033fbdc20 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -2,7 +2,7 @@ anthropic = "2.40.1" kotlin = "2.3.21" ktor = "3.3.3" -mcp-kotlin = "0.12.0" +mcp-kotlin = "0.13.0" shadow = "9.4.2" slf4j = "2.0.18" From b71df8be126deceab628c701a79b03fb3177a697 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:54:57 +0000 Subject: [PATCH 164/187] chore(deps): bump @types/node from 25.9.1 to 25.9.3 in /integration-test/src/jvmTest/typescript in the all-dependencies group across 1 directory (#821) Bumps the all-dependencies group with 1 update in the /integration-test/src/jvmTest/typescript directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 25.9.1 to 25.9.3
    Commits

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 8 ++++---- integration-test/src/jvmTest/typescript/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index ed07e4ea3..3dab6230d 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -15,7 +15,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.9.1", + "@types/node": "^25.9.3", "cors": "^2.8.6", "tsx": "^4.22.4", "typescript": "^6.0.3" @@ -579,9 +579,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index 0fafdb92c..629b3bb36 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.9.1", + "@types/node": "^25.9.3", "cors": "^2.8.6", "tsx": "^4.22.4", "typescript": "^6.0.3" From 2b9bc78165f85aff0c1eec99c483af3e87dbe312 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:13:14 +0000 Subject: [PATCH 165/187] chore(deps): bump mcp-kotlin from 0.12.0 to 0.13.0 in /samples/kotlin-mcp-server (#823) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.12.0 to 0.13.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.12.0 to 0.13.0
    Release notes

    Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

    0.13.0

    Description

    Adds a tasks capability and a typed elicitation capability, hardens transport lifecycle, dispatchers, and back-pressure, and enables DNS rebinding protection by default.

    Breaking Changes

    Typed elicitation capability and new tasks capability by @​devcrocod in #732

    ClientCapabilities.elicitation is now a typed ClientCapabilities.Elicitation (form, url) instead of a raw JsonObject?, and both ClientCapabilities and ServerCapabilities gained a typed tasks capability. The constructors of both types changed and are binary-incompatible; recompilation is required.

    Transport handlers now run on Dispatchers.Default by @​devcrocod in #778

    Handlers moved from Dispatchers.IO to Dispatchers.Default (override via handlerDispatcher), send() now suspends under back-pressure instead of buffering unbounded, and the two-argument StdioServerTransport(input, output) constructor is deprecated in favor of the builder-lambda form. Also fixes a start()/close() race, consistent CancellationException propagation, and an input-Source leak (closes #573, #574, #708).

    DNS rebinding protection enabled by default by @​devcrocod in #659

    enableDnsRebindingProtection now defaults to true on mcpStreamableHttp/mcpStatelessStreamableHttp, and the Route.mcp() / Application.mcp() overloads gained enableDnsRebindingProtection, allowedHosts, and allowedOrigins parameters (binary-incompatible on JVM). Only localhost, 127.0.0.1, and [::1] are allowed by default; opt out with enableDnsRebindingProtection = false. The Configuration.enableDnsRebindingProtection, .allowedHosts, and .allowedOrigins properties are deprecated in favor of the new DnsRebindingProtection plugin.

    Features

    • Add a tasks capability (list, cancel, and per-request augmentation for sampling and elicitation) on ClientCapabilities and ServerCapabilities, with assertions for tasks/* and notifications/tasks/status enforced by Client and ServerSession by @​devcrocod in #732
    • Configurable handlerDispatcher and ioDispatcher, plus an optional caller-supplied CoroutineScope, on transports by @​devcrocod in #778

    Fixed

    • Fire onClose callbacks on SseClientTransport when the SSE session ends — whether closed gracefully or terminated by an error — matching the other client transports by @​jskjw157 in #738

    Security

    • Enable DNS rebinding protection by default across all HTTP transports, including the SSE endpoints that previously had none (see Breaking Changes for migration) by @​devcrocod in #659
    • Cap a single stdio JSON-RPC frame at 16 MiB, throwing the new TooLongFrameException instead of buffering unbounded when a peer never sends a newline terminator by @​devcrocod in 6e6f805

    Maintenance

    • Remove the test-only dev.mokksy:mokksy dependency in favor of MockEngine and embeddedServer by @​devcrocod in #754

    Dependencies

    • Ktor to v3.4.3 in #785
    • kotlinx.coroutines to v1.11.0 in #760
    • kotlin-logging to v8.0.03 in #752, #783
    • 24 further Dependabot updates across sample projects, TypeScript conformance-test fixtures, and build/test tooling (JUnit, slf4j-simple, Gradle wrapper, anthropic-java)

    Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.12.0...0.13.0

    Commits
    • b7eeeb1 release: 0.13.0 (#796)
    • 6e6f805 Merge commit from fork
    • 0eb1822 Update Ktor to version 3.4.3 (#785)
    • 0adfb3b fix!: harden lifecycle, dispatchers, back-pressure (#778)
    • 37ee423 chore(deps): bump the infrastructure group with 2 updates (#783)
    • ff4b623 chore(deps): bump the other-dependencies group across 3 directories with 3 up...
    • 78066a9 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • 1ff5f00 chore(deps): bump qs from 6.14.2 to 6.15.2 in /integration-test/src/jvmTest/t...
    • 6d5bac1 chore(deps): bump gradle-wrapper from 9.5.0 to 9.5.1 (#776)
    • c854560 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • Additional commits viewable in compare view

    Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.12.0 to 0.13.0
    Release notes

    Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

    0.13.0

    Description

    Adds a tasks capability and a typed elicitation capability, hardens transport lifecycle, dispatchers, and back-pressure, and enables DNS rebinding protection by default.

    Breaking Changes

    Typed elicitation capability and new tasks capability by @​devcrocod in #732

    ClientCapabilities.elicitation is now a typed ClientCapabilities.Elicitation (form, url) instead of a raw JsonObject?, and both ClientCapabilities and ServerCapabilities gained a typed tasks capability. The constructors of both types changed and are binary-incompatible; recompilation is required.

    Transport handlers now run on Dispatchers.Default by @​devcrocod in #778

    Handlers moved from Dispatchers.IO to Dispatchers.Default (override via handlerDispatcher), send() now suspends under back-pressure instead of buffering unbounded, and the two-argument StdioServerTransport(input, output) constructor is deprecated in favor of the builder-lambda form. Also fixes a start()/close() race, consistent CancellationException propagation, and an input-Source leak (closes #573, #574, #708).

    DNS rebinding protection enabled by default by @​devcrocod in #659

    enableDnsRebindingProtection now defaults to true on mcpStreamableHttp/mcpStatelessStreamableHttp, and the Route.mcp() / Application.mcp() overloads gained enableDnsRebindingProtection, allowedHosts, and allowedOrigins parameters (binary-incompatible on JVM). Only localhost, 127.0.0.1, and [::1] are allowed by default; opt out with enableDnsRebindingProtection = false. The Configuration.enableDnsRebindingProtection, .allowedHosts, and .allowedOrigins properties are deprecated in favor of the new DnsRebindingProtection plugin.

    Features

    • Add a tasks capability (list, cancel, and per-request augmentation for sampling and elicitation) on ClientCapabilities and ServerCapabilities, with assertions for tasks/* and notifications/tasks/status enforced by Client and ServerSession by @​devcrocod in #732
    • Configurable handlerDispatcher and ioDispatcher, plus an optional caller-supplied CoroutineScope, on transports by @​devcrocod in #778

    Fixed

    • Fire onClose callbacks on SseClientTransport when the SSE session ends — whether closed gracefully or terminated by an error — matching the other client transports by @​jskjw157 in #738

    Security

    • Enable DNS rebinding protection by default across all HTTP transports, including the SSE endpoints that previously had none (see Breaking Changes for migration) by @​devcrocod in #659
    • Cap a single stdio JSON-RPC frame at 16 MiB, throwing the new TooLongFrameException instead of buffering unbounded when a peer never sends a newline terminator by @​devcrocod in 6e6f805

    Maintenance

    • Remove the test-only dev.mokksy:mokksy dependency in favor of MockEngine and embeddedServer by @​devcrocod in #754

    Dependencies

    • Ktor to v3.4.3 in #785
    • kotlinx.coroutines to v1.11.0 in #760
    • kotlin-logging to v8.0.03 in #752, #783
    • 24 further Dependabot updates across sample projects, TypeScript conformance-test fixtures, and build/test tooling (JUnit, slf4j-simple, Gradle wrapper, anthropic-java)

    Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.12.0...0.13.0

    Commits
    • b7eeeb1 release: 0.13.0 (#796)
    • 6e6f805 Merge commit from fork
    • 0eb1822 Update Ktor to version 3.4.3 (#785)
    • 0adfb3b fix!: harden lifecycle, dispatchers, back-pressure (#778)
    • 37ee423 chore(deps): bump the infrastructure group with 2 updates (#783)
    • ff4b623 chore(deps): bump the other-dependencies group across 3 directories with 3 up...
    • 78066a9 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • 1ff5f00 chore(deps): bump qs from 6.14.2 to 6.15.2 in /integration-test/src/jvmTest/t...
    • 6d5bac1 chore(deps): bump gradle-wrapper from 9.5.0 to 9.5.1 (#776)
    • c854560 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • Additional commits viewable in compare view

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index 165d09616..bd87373d9 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.3.21" ktor = "3.3.3" -mcp-kotlin = "0.12.0" +mcp-kotlin = "0.13.0" slf4j = "2.0.18" shadow = "9.4.2" From 251bacdc447d21a0c0031aaaca8bfd6c27f5c3aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:27:40 +0000 Subject: [PATCH 166/187] chore(deps): bump mcp-kotlin from 0.12.0 to 0.13.0 in /samples/simple-streamable-server (#826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.12.0 to 0.13.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.12.0 to 0.13.0
    Release notes

    Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

    0.13.0

    Description

    Adds a tasks capability and a typed elicitation capability, hardens transport lifecycle, dispatchers, and back-pressure, and enables DNS rebinding protection by default.

    Breaking Changes

    Typed elicitation capability and new tasks capability by @​devcrocod in #732

    ClientCapabilities.elicitation is now a typed ClientCapabilities.Elicitation (form, url) instead of a raw JsonObject?, and both ClientCapabilities and ServerCapabilities gained a typed tasks capability. The constructors of both types changed and are binary-incompatible; recompilation is required.

    Transport handlers now run on Dispatchers.Default by @​devcrocod in #778

    Handlers moved from Dispatchers.IO to Dispatchers.Default (override via handlerDispatcher), send() now suspends under back-pressure instead of buffering unbounded, and the two-argument StdioServerTransport(input, output) constructor is deprecated in favor of the builder-lambda form. Also fixes a start()/close() race, consistent CancellationException propagation, and an input-Source leak (closes #573, #574, #708).

    DNS rebinding protection enabled by default by @​devcrocod in #659

    enableDnsRebindingProtection now defaults to true on mcpStreamableHttp/mcpStatelessStreamableHttp, and the Route.mcp() / Application.mcp() overloads gained enableDnsRebindingProtection, allowedHosts, and allowedOrigins parameters (binary-incompatible on JVM). Only localhost, 127.0.0.1, and [::1] are allowed by default; opt out with enableDnsRebindingProtection = false. The Configuration.enableDnsRebindingProtection, .allowedHosts, and .allowedOrigins properties are deprecated in favor of the new DnsRebindingProtection plugin.

    Features

    • Add a tasks capability (list, cancel, and per-request augmentation for sampling and elicitation) on ClientCapabilities and ServerCapabilities, with assertions for tasks/* and notifications/tasks/status enforced by Client and ServerSession by @​devcrocod in #732
    • Configurable handlerDispatcher and ioDispatcher, plus an optional caller-supplied CoroutineScope, on transports by @​devcrocod in #778

    Fixed

    • Fire onClose callbacks on SseClientTransport when the SSE session ends — whether closed gracefully or terminated by an error — matching the other client transports by @​jskjw157 in #738

    Security

    • Enable DNS rebinding protection by default across all HTTP transports, including the SSE endpoints that previously had none (see Breaking Changes for migration) by @​devcrocod in #659
    • Cap a single stdio JSON-RPC frame at 16 MiB, throwing the new TooLongFrameException instead of buffering unbounded when a peer never sends a newline terminator by @​devcrocod in 6e6f805

    Maintenance

    • Remove the test-only dev.mokksy:mokksy dependency in favor of MockEngine and embeddedServer by @​devcrocod in #754

    Dependencies

    • Ktor to v3.4.3 in #785
    • kotlinx.coroutines to v1.11.0 in #760
    • kotlin-logging to v8.0.03 in #752, #783
    • 24 further Dependabot updates across sample projects, TypeScript conformance-test fixtures, and build/test tooling (JUnit, slf4j-simple, Gradle wrapper, anthropic-java)

    Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.12.0...0.13.0

    Commits
    • b7eeeb1 release: 0.13.0 (#796)
    • 6e6f805 Merge commit from fork
    • 0eb1822 Update Ktor to version 3.4.3 (#785)
    • 0adfb3b fix!: harden lifecycle, dispatchers, back-pressure (#778)
    • 37ee423 chore(deps): bump the infrastructure group with 2 updates (#783)
    • ff4b623 chore(deps): bump the other-dependencies group across 3 directories with 3 up...
    • 78066a9 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • 1ff5f00 chore(deps): bump qs from 6.14.2 to 6.15.2 in /integration-test/src/jvmTest/t...
    • 6d5bac1 chore(deps): bump gradle-wrapper from 9.5.0 to 9.5.1 (#776)
    • c854560 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • Additional commits viewable in compare view

    Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.12.0 to 0.13.0
    Release notes

    Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

    0.13.0

    Description

    Adds a tasks capability and a typed elicitation capability, hardens transport lifecycle, dispatchers, and back-pressure, and enables DNS rebinding protection by default.

    Breaking Changes

    Typed elicitation capability and new tasks capability by @​devcrocod in #732

    ClientCapabilities.elicitation is now a typed ClientCapabilities.Elicitation (form, url) instead of a raw JsonObject?, and both ClientCapabilities and ServerCapabilities gained a typed tasks capability. The constructors of both types changed and are binary-incompatible; recompilation is required.

    Transport handlers now run on Dispatchers.Default by @​devcrocod in #778

    Handlers moved from Dispatchers.IO to Dispatchers.Default (override via handlerDispatcher), send() now suspends under back-pressure instead of buffering unbounded, and the two-argument StdioServerTransport(input, output) constructor is deprecated in favor of the builder-lambda form. Also fixes a start()/close() race, consistent CancellationException propagation, and an input-Source leak (closes #573, #574, #708).

    DNS rebinding protection enabled by default by @​devcrocod in #659

    enableDnsRebindingProtection now defaults to true on mcpStreamableHttp/mcpStatelessStreamableHttp, and the Route.mcp() / Application.mcp() overloads gained enableDnsRebindingProtection, allowedHosts, and allowedOrigins parameters (binary-incompatible on JVM). Only localhost, 127.0.0.1, and [::1] are allowed by default; opt out with enableDnsRebindingProtection = false. The Configuration.enableDnsRebindingProtection, .allowedHosts, and .allowedOrigins properties are deprecated in favor of the new DnsRebindingProtection plugin.

    Features

    • Add a tasks capability (list, cancel, and per-request augmentation for sampling and elicitation) on ClientCapabilities and ServerCapabilities, with assertions for tasks/* and notifications/tasks/status enforced by Client and ServerSession by @​devcrocod in #732
    • Configurable handlerDispatcher and ioDispatcher, plus an optional caller-supplied CoroutineScope, on transports by @​devcrocod in #778

    Fixed

    • Fire onClose callbacks on SseClientTransport when the SSE session ends — whether closed gracefully or terminated by an error — matching the other client transports by @​jskjw157 in #738

    Security

    • Enable DNS rebinding protection by default across all HTTP transports, including the SSE endpoints that previously had none (see Breaking Changes for migration) by @​devcrocod in #659
    • Cap a single stdio JSON-RPC frame at 16 MiB, throwing the new TooLongFrameException instead of buffering unbounded when a peer never sends a newline terminator by @​devcrocod in 6e6f805

    Maintenance

    • Remove the test-only dev.mokksy:mokksy dependency in favor of MockEngine and embeddedServer by @​devcrocod in #754

    Dependencies

    • Ktor to v3.4.3 in #785
    • kotlinx.coroutines to v1.11.0 in #760
    • kotlin-logging to v8.0.03 in #752, #783
    • 24 further Dependabot updates across sample projects, TypeScript conformance-test fixtures, and build/test tooling (JUnit, slf4j-simple, Gradle wrapper, anthropic-java)

    Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.12.0...0.13.0

    Commits
    • b7eeeb1 release: 0.13.0 (#796)
    • 6e6f805 Merge commit from fork
    • 0eb1822 Update Ktor to version 3.4.3 (#785)
    • 0adfb3b fix!: harden lifecycle, dispatchers, back-pressure (#778)
    • 37ee423 chore(deps): bump the infrastructure group with 2 updates (#783)
    • ff4b623 chore(deps): bump the other-dependencies group across 3 directories with 3 up...
    • 78066a9 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • 1ff5f00 chore(deps): bump qs from 6.14.2 to 6.15.2 in /integration-test/src/jvmTest/t...
    • 6d5bac1 chore(deps): bump gradle-wrapper from 9.5.0 to 9.5.1 (#776)
    • c854560 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • Additional commits viewable in compare view

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/simple-streamable-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index 07bdac1d0..ba5b0e7c0 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin = "2.3.21" ktor = "3.3.3" junit = "6.1.0" -mcp-kotlin = "0.12.0" +mcp-kotlin = "0.13.0" slf4j = "2.0.18" shadow = "9.4.2" From 479527c7bb2274c50c3ce112801c3ee242d3fb69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:18:19 +0200 Subject: [PATCH 167/187] chore(deps): bump mcp-kotlin from 0.12.0 to 0.13.0 in /samples/weather-stdio-server (#828) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.12.0 to 0.13.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.12.0 to 0.13.0
    Release notes

    Sourced from io.modelcontextprotocol:kotlin-sdk-client's releases.

    0.13.0

    Description

    Adds a tasks capability and a typed elicitation capability, hardens transport lifecycle, dispatchers, and back-pressure, and enables DNS rebinding protection by default.

    Breaking Changes

    Typed elicitation capability and new tasks capability by @​devcrocod in #732

    ClientCapabilities.elicitation is now a typed ClientCapabilities.Elicitation (form, url) instead of a raw JsonObject?, and both ClientCapabilities and ServerCapabilities gained a typed tasks capability. The constructors of both types changed and are binary-incompatible; recompilation is required.

    Transport handlers now run on Dispatchers.Default by @​devcrocod in #778

    Handlers moved from Dispatchers.IO to Dispatchers.Default (override via handlerDispatcher), send() now suspends under back-pressure instead of buffering unbounded, and the two-argument StdioServerTransport(input, output) constructor is deprecated in favor of the builder-lambda form. Also fixes a start()/close() race, consistent CancellationException propagation, and an input-Source leak (closes #573, #574, #708).

    DNS rebinding protection enabled by default by @​devcrocod in #659

    enableDnsRebindingProtection now defaults to true on mcpStreamableHttp/mcpStatelessStreamableHttp, and the Route.mcp() / Application.mcp() overloads gained enableDnsRebindingProtection, allowedHosts, and allowedOrigins parameters (binary-incompatible on JVM). Only localhost, 127.0.0.1, and [::1] are allowed by default; opt out with enableDnsRebindingProtection = false. The Configuration.enableDnsRebindingProtection, .allowedHosts, and .allowedOrigins properties are deprecated in favor of the new DnsRebindingProtection plugin.

    Features

    • Add a tasks capability (list, cancel, and per-request augmentation for sampling and elicitation) on ClientCapabilities and ServerCapabilities, with assertions for tasks/* and notifications/tasks/status enforced by Client and ServerSession by @​devcrocod in #732
    • Configurable handlerDispatcher and ioDispatcher, plus an optional caller-supplied CoroutineScope, on transports by @​devcrocod in #778

    Fixed

    • Fire onClose callbacks on SseClientTransport when the SSE session ends — whether closed gracefully or terminated by an error — matching the other client transports by @​jskjw157 in #738

    Security

    • Enable DNS rebinding protection by default across all HTTP transports, including the SSE endpoints that previously had none (see Breaking Changes for migration) by @​devcrocod in #659
    • Cap a single stdio JSON-RPC frame at 16 MiB, throwing the new TooLongFrameException instead of buffering unbounded when a peer never sends a newline terminator by @​devcrocod in 6e6f805

    Maintenance

    • Remove the test-only dev.mokksy:mokksy dependency in favor of MockEngine and embeddedServer by @​devcrocod in #754

    Dependencies

    • Ktor to v3.4.3 in #785
    • kotlinx.coroutines to v1.11.0 in #760
    • kotlin-logging to v8.0.03 in #752, #783
    • 24 further Dependabot updates across sample projects, TypeScript conformance-test fixtures, and build/test tooling (JUnit, slf4j-simple, Gradle wrapper, anthropic-java)

    Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.12.0...0.13.0

    Commits
    • b7eeeb1 release: 0.13.0 (#796)
    • 6e6f805 Merge commit from fork
    • 0eb1822 Update Ktor to version 3.4.3 (#785)
    • 0adfb3b fix!: harden lifecycle, dispatchers, back-pressure (#778)
    • 37ee423 chore(deps): bump the infrastructure group with 2 updates (#783)
    • ff4b623 chore(deps): bump the other-dependencies group across 3 directories with 3 up...
    • 78066a9 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • 1ff5f00 chore(deps): bump qs from 6.14.2 to 6.15.2 in /integration-test/src/jvmTest/t...
    • 6d5bac1 chore(deps): bump gradle-wrapper from 9.5.0 to 9.5.1 (#776)
    • c854560 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • Additional commits viewable in compare view

    Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.12.0 to 0.13.0
    Release notes

    Sourced from io.modelcontextprotocol:kotlin-sdk-server's releases.

    0.13.0

    Description

    Adds a tasks capability and a typed elicitation capability, hardens transport lifecycle, dispatchers, and back-pressure, and enables DNS rebinding protection by default.

    Breaking Changes

    Typed elicitation capability and new tasks capability by @​devcrocod in #732

    ClientCapabilities.elicitation is now a typed ClientCapabilities.Elicitation (form, url) instead of a raw JsonObject?, and both ClientCapabilities and ServerCapabilities gained a typed tasks capability. The constructors of both types changed and are binary-incompatible; recompilation is required.

    Transport handlers now run on Dispatchers.Default by @​devcrocod in #778

    Handlers moved from Dispatchers.IO to Dispatchers.Default (override via handlerDispatcher), send() now suspends under back-pressure instead of buffering unbounded, and the two-argument StdioServerTransport(input, output) constructor is deprecated in favor of the builder-lambda form. Also fixes a start()/close() race, consistent CancellationException propagation, and an input-Source leak (closes #573, #574, #708).

    DNS rebinding protection enabled by default by @​devcrocod in #659

    enableDnsRebindingProtection now defaults to true on mcpStreamableHttp/mcpStatelessStreamableHttp, and the Route.mcp() / Application.mcp() overloads gained enableDnsRebindingProtection, allowedHosts, and allowedOrigins parameters (binary-incompatible on JVM). Only localhost, 127.0.0.1, and [::1] are allowed by default; opt out with enableDnsRebindingProtection = false. The Configuration.enableDnsRebindingProtection, .allowedHosts, and .allowedOrigins properties are deprecated in favor of the new DnsRebindingProtection plugin.

    Features

    • Add a tasks capability (list, cancel, and per-request augmentation for sampling and elicitation) on ClientCapabilities and ServerCapabilities, with assertions for tasks/* and notifications/tasks/status enforced by Client and ServerSession by @​devcrocod in #732
    • Configurable handlerDispatcher and ioDispatcher, plus an optional caller-supplied CoroutineScope, on transports by @​devcrocod in #778

    Fixed

    • Fire onClose callbacks on SseClientTransport when the SSE session ends — whether closed gracefully or terminated by an error — matching the other client transports by @​jskjw157 in #738

    Security

    • Enable DNS rebinding protection by default across all HTTP transports, including the SSE endpoints that previously had none (see Breaking Changes for migration) by @​devcrocod in #659
    • Cap a single stdio JSON-RPC frame at 16 MiB, throwing the new TooLongFrameException instead of buffering unbounded when a peer never sends a newline terminator by @​devcrocod in 6e6f805

    Maintenance

    • Remove the test-only dev.mokksy:mokksy dependency in favor of MockEngine and embeddedServer by @​devcrocod in #754

    Dependencies

    • Ktor to v3.4.3 in #785
    • kotlinx.coroutines to v1.11.0 in #760
    • kotlin-logging to v8.0.03 in #752, #783
    • 24 further Dependabot updates across sample projects, TypeScript conformance-test fixtures, and build/test tooling (JUnit, slf4j-simple, Gradle wrapper, anthropic-java)

    Full Changelog: https://github.com/modelcontextprotocol/kotlin-sdk/compare/0.12.0...0.13.0

    Commits
    • b7eeeb1 release: 0.13.0 (#796)
    • 6e6f805 Merge commit from fork
    • 0eb1822 Update Ktor to version 3.4.3 (#785)
    • 0adfb3b fix!: harden lifecycle, dispatchers, back-pressure (#778)
    • 37ee423 chore(deps): bump the infrastructure group with 2 updates (#783)
    • ff4b623 chore(deps): bump the other-dependencies group across 3 directories with 3 up...
    • 78066a9 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • 1ff5f00 chore(deps): bump qs from 6.14.2 to 6.15.2 in /integration-test/src/jvmTest/t...
    • 6d5bac1 chore(deps): bump gradle-wrapper from 9.5.0 to 9.5.1 (#776)
    • c854560 chore(deps): bump the all-dependencies group across 1 directory with 2 update...
    • Additional commits viewable in compare view

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index d7a6fcbff..3e99eadae 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ coroutines = "1.11.0" kotlin = "2.3.21" ktor = "3.3.3" logging = "8.0.4" -mcp-kotlin = "0.12.0" +mcp-kotlin = "0.13.0" shadow = "9.4.2" slf4j = "2.0.18" From d65c50735b6b8753a6db2fa3fc489ed24ed1f5c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:36:48 +0200 Subject: [PATCH 168/187] chore(deps): bump esbuild from 0.28.0 to 0.28.1 in /integration-test/src/jvmTest/typescript (#831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [esbuild](https://github.com/evanw/esbuild) from 0.28.0 to 0.28.1.
    Release notes

    Sourced from esbuild's releases.

    v0.28.1

    • Disallow \ in local development server HTTP requests (GHSA-g7r4-m6w7-qqqr)

      This release fixes a security issue where HTTP requests to esbuild's local development server could traverse outside of the serve directory on Windows using a \ backslash character. It happened due to the use of Go's path.Clean() function, which only handles Unix-style / characters. HTTP requests with paths containing \ are no longer allowed.

      Thanks to @​dellalibera for reporting this issue.

    • Add integrity checks to the Deno API (GHSA-gv7w-rqvm-qjhr)

      The previous release of esbuild added integrity checks to esbuild's npm install script. This release also adds integrity checks to esbuild's Deno install script. Now esbuild's Deno API will also fail with an error if the downloaded esbuild binary contains something other than the expected content.

      Note that esbuild's Deno API installs from registry.npmjs.org by default, but allows the NPM_CONFIG_REGISTRY environment variable to override this with a custom package registry. This change means that the esbuild executable served by NPM_CONFIG_REGISTRY must now match the expected content.

      Thanks to @​sondt99 for reporting this issue.

    • Avoid inlining using and await using declarations (#4482)

      Previously esbuild's minifier sometimes incorrectly inlined using and await using declarations into subsequent uses of that declaration, which then fails to dispose of the resource correctly. This bug happened because inlining was done for let and const declarations by avoiding doing it for var declarations, which no longer worked when more declaration types were added. Here's an example:

      // Original code
      {
        using x = new Resource()
        x.activate()
      }
      

      // Old output (with --minify)
      new Resource().activate();

      // New output (with --minify)
      {using e=new Resource;e.activate()}

    • Fix module evaluation when an error is thrown (#4461, #4467)

      If an error is thrown during module evaluation, esbuild previously didn't preserve the state of the module for subsequent module references. This was observable if import() or require() is used to import a module multiple times. The thrown error is supposed to be thrown by every call to import() or require(), not just the first. With this release, esbuild will now throw the same error every time you call import() or require() on a module that throws during its evaluation.

    • Fix some edge cases around the new operator (#4477)

      Previously esbuild incorrectly printed certain edge cases involving complex expressions inside the target of a new expression (specifically an optional chain and/or a tagged template literal). The generated code for the new target was not correctly wrapped with parentheses, and either contained a syntax error or had different semantics. These edge cases have been fixed so that they now correctly wrap the new target in parentheses. Here is an example of some affected code:

      // Original code
      new (foo()`bar`)()
      new (foo()?.bar)()
      

      // Old output
      new foo()bar();
      new (foo())?.bar();

    ... (truncated)

    Changelog

    Sourced from esbuild's changelog.

    0.28.1

    • Disallow \ in local development server HTTP requests (GHSA-g7r4-m6w7-qqqr)

      This release fixes a security issue where HTTP requests to esbuild's local development server could traverse outside of the serve directory on Windows using a \ backslash character. It happened due to the use of Go's path.Clean() function, which only handles Unix-style / characters. HTTP requests with paths containing \ are no longer allowed.

      Thanks to @​dellalibera for reporting this issue.

    • Add integrity checks to the Deno API (GHSA-gv7w-rqvm-qjhr)

      The previous release of esbuild added integrity checks to esbuild's npm install script. This release also adds integrity checks to esbuild's Deno install script. Now esbuild's Deno API will also fail with an error if the downloaded esbuild binary contains something other than the expected content.

      Note that esbuild's Deno API installs from registry.npmjs.org by default, but allows the NPM_CONFIG_REGISTRY environment variable to override this with a custom package registry. This change means that the esbuild executable served by NPM_CONFIG_REGISTRY must now match the expected content.

      Thanks to @​sondt99 for reporting this issue.

    • Avoid inlining using and await using declarations (#4482)

      Previously esbuild's minifier sometimes incorrectly inlined using and await using declarations into subsequent uses of that declaration, which then fails to dispose of the resource correctly. This bug happened because inlining was done for let and const declarations by avoiding doing it for var declarations, which no longer worked when more declaration types were added. Here's an example:

      // Original code
      {
        using x = new Resource()
        x.activate()
      }
      

      // Old output (with --minify)
      new Resource().activate();

      // New output (with --minify)
      {using e=new Resource;e.activate()}

    • Fix module evaluation when an error is thrown (#4461, #4467)

      If an error is thrown during module evaluation, esbuild previously didn't preserve the state of the module for subsequent module references. This was observable if import() or require() is used to import a module multiple times. The thrown error is supposed to be thrown by every call to import() or require(), not just the first. With this release, esbuild will now throw the same error every time you call import() or require() on a module that throws during its evaluation.

    • Fix some edge cases around the new operator (#4477)

      Previously esbuild incorrectly printed certain edge cases involving complex expressions inside the target of a new expression (specifically an optional chain and/or a tagged template literal). The generated code for the new target was not correctly wrapped with parentheses, and either contained a syntax error or had different semantics. These edge cases have been fixed so that they now correctly wrap the new target in parentheses. Here is an example of some affected code:

      // Original code
      new (foo()`bar`)()
      new (foo()?.bar)()
      

      // Old output
      new foo()bar();
      new (foo())?.bar();

    ... (truncated)

    Commits

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/jvmTest/typescript/package-lock.json | 214 +++++++++--------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 3dab6230d..de65fb226 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -22,9 +22,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -39,9 +39,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -56,9 +56,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -73,9 +73,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -90,9 +90,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -107,9 +107,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -124,9 +124,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -141,9 +141,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -158,9 +158,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -175,9 +175,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -192,9 +192,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -209,9 +209,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -226,9 +226,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -243,9 +243,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -260,9 +260,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -277,9 +277,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -294,9 +294,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -311,9 +311,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -328,9 +328,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -345,9 +345,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -362,9 +362,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -379,9 +379,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -396,9 +396,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -413,9 +413,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -430,9 +430,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -447,9 +447,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -888,9 +888,9 @@ } }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -901,32 +901,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escape-html": { From 75a944fb8ce249eb36a6800eabeddca7cf8b40e2 Mon Sep 17 00:00:00 2001 From: Mukunda Rao Katta Date: Wed, 17 Jun 2026 10:11:23 -0700 Subject: [PATCH 169/187] fix: polish SEP-1577 sampling handling (#765) ## Summary - reject empty sampling content in SamplingMessage and CreateMessageResult constructors - clarify sampling validation docs and emit an explicit error when tool_use is not followed by tool_result - return sampled text from the conformance test_sampling tool instead of content object strings Fixes #730 ## Testing - ./gradlew -Dorg.gradle.java.installations.paths="/Users/ubl/Documents/New project/oss-contributions/.jdks/jdk-21.0.11+10/Contents/Home" :kotlin-sdk-core:jvmTest :kotlin-sdk-server:jvmTest :conformance-test:compileKotlin Co-authored-by: Pavel Gorgulov --- .../kotlin/sdk/conformance/ConformanceTools.kt | 8 +++++++- .../kotlin/sdk/types/sampling.kt | 8 ++++++++ .../kotlin/sdk/types/SamplingTest.kt | 18 ++++++++++++++++++ .../kotlin/sdk/server/SamplingValidation.kt | 7 +++++-- .../kotlin/sdk/server/SamplingTest.kt | 18 ++++++++++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt index e288272ef..c81c98c31 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt @@ -179,7 +179,13 @@ fun Server.registerConformanceTools() { ), ), ) - CallToolResult(listOf(TextContent(result.content.joinToString("\n") { it.toString() }))) + val sampledText = result.content.joinToString("\n") { content -> + when (content) { + is TextContent -> content.text + else -> "Non-text sampling content: ${content::class.simpleName}" + } + } + CallToolResult(listOf(TextContent(sampledText))) } // 9. Elicitation diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt index 211fdc798..452640598 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt @@ -91,6 +91,10 @@ public data class SamplingMessage( @SerialName("_meta") val meta: JsonObject? = null, ) { + init { + require(content.isNotEmpty()) { "content must contain at least one block" } + } + /** * Convenience constructor for a single-block message. Wraps [content] in a * singleton list so call sites can write `SamplingMessage(Role.User, TextContent("hi"))` @@ -273,6 +277,10 @@ public data class CreateMessageResult( @SerialName("_meta") override val meta: JsonObject? = null, ) : ClientResult { + init { + require(content.isNotEmpty()) { "content must contain at least one block" } + } + /** * Convenience constructor for a single-block response. Wraps [content] in a * singleton list so call sites can write diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt index 62a82cd4f..b2c8cf0cd 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt @@ -302,6 +302,13 @@ class SamplingTest { (m.content[0] as TextContent).text shouldBe "hi" } + @Test + fun `SamplingMessage rejects empty content`() { + assertFailsWith { + SamplingMessage(role = Role.User, content = emptyList()) + } + } + @Test fun `SamplingMessage single-element content serialises as single object`() { val m = SamplingMessage(role = Role.User, content = listOf(TextContent("hi"))) @@ -408,6 +415,17 @@ class SamplingTest { (decoded.content[0] as TextContent).text shouldBe "hi" } + @Test + fun `CreateMessageResult rejects empty content`() { + assertFailsWith { + CreateMessageResult( + role = Role.Assistant, + content = emptyList(), + model = "test-model", + ) + } + } + // ============================================================================ // SamplingContentSerializer (single-or-array wire heuristic) // ============================================================================ diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt index ef408080a..824317133 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt @@ -20,8 +20,8 @@ import io.modelcontextprotocol.kotlin.sdk.types.ToolUseContent * 3. If the previous message contains `tool_use` blocks, the last message's * `tool_result` ids MUST form exactly the same set. * - * On the first violation throws [IllegalArgumentException]. No-op when there are fewer - * than two messages or no tool_use / tool_result blocks are involved. + * On the first violation throws [IllegalArgumentException]. No-op when no + * tool_use / tool_result blocks are involved. */ internal fun validateSamplingMessages(messages: List) { if (messages.isEmpty()) return @@ -44,6 +44,9 @@ internal fun validateSamplingMessages(messages: List) { if (hasPreviousToolUse) { val toolUseIds = previous.filterIsInstance().map { it.id }.toSet() val toolResultIds = last.filterIsInstance().map { it.toolUseId }.toSet() + require(toolResultIds.isNotEmpty()) { + "tool_use blocks from previous message must be followed by matching tool_result blocks" + } require(toolUseIds == toolResultIds) { "ids of tool_result blocks and tool_use blocks from previous message do not match" } diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt index 2b5446bfa..cfcbb62c8 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt @@ -8,6 +8,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ToolUseContent import kotlinx.serialization.json.JsonObject import org.junit.jupiter.api.assertDoesNotThrow import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFailsWith /** @@ -83,4 +84,21 @@ class SamplingTest { ) } } + + @Test + fun `validate tool_use requires explicit tool_result in last message`() { + val error = assertFailsWith { + validateSamplingMessages( + listOf( + SamplingMessage(Role.Assistant, toolUse("c1")), + SamplingMessage(Role.User, TextContent("missing result")), + ), + ) + } + + assertEquals( + "tool_use blocks from previous message must be followed by matching tool_result blocks", + error.message, + ) + } } From d21728eccab4b59a2a850eb417695c624d95bab4 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Thu, 18 Jun 2026 08:54:54 +0200 Subject: [PATCH 170/187] fix: bound inline SSE event size in Streamable HTTP client (#841) Bound the size of a single inline SSE event parsed from a POST response in `StreamableHttpClientTransport`, so a non-conforming or unbounded server response cannot grow the client's buffer without limit. ## How Has This Been Tested? New unit tests ## Breaking Changes none ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- kotlin-sdk-client/api/kotlin-sdk-client.api | 4 +- .../client/StreamableHttpClientTransport.kt | 42 ++++++- .../http/StreamableHttpClientTransportTest.kt | 106 +++++++++++++++++- 3 files changed, 144 insertions(+), 8 deletions(-) diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index 8b6fb79b4..0a544f510 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -101,8 +101,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTranspor } public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport { - public fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;ILkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;ILkotlin/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;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 final fun getProtocolVersion ()Ljava/lang/String; diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt index c27ed79e1..a63c1d265 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt @@ -21,8 +21,10 @@ import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.http.contentType import io.ktor.http.isSuccess +import io.ktor.utils.io.charsets.TooLongLineException import io.ktor.utils.io.readUTF8Line import io.modelcontextprotocol.kotlin.sdk.shared.AbstractClientTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TooLongFrameException import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification @@ -50,6 +52,14 @@ private const val MCP_SESSION_ID_HEADER = "mcp-session-id" private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID" +/** + * Default maximum size, in characters, of a single inline SSE event assembled from a POST response. + * + * Mirrors the stdio transport's 16 MiB frame cap: a server that streams `data:` lines without ever + * terminating the event cannot grow the client's buffer without bound. + */ +private const val DEFAULT_MAX_INLINE_SSE_EVENT_SIZE: Int = 16 * 1024 * 1024 + /** * Represents an error from the Streamable HTTP transport. * @@ -75,15 +85,23 @@ private sealed interface ConnectResult { * @param client Ktor HTTP client used for all requests * @param url MCP endpoint URL * @param reconnectionOptions reconnection backoff and retry-limit settings for the SSE stream + * @param maxInlineSseEventSize maximum size, in characters, of a single inline SSE event parsed from a + * POST response; a server that exceeds it (including by never terminating an event) fails the send + * with [io.modelcontextprotocol.kotlin.sdk.shared.TooLongFrameException]. Defaults to 16 MiB. * @param requestBuilder builder applied to every outgoing HTTP request, e.g. for adding auth headers */ public class StreamableHttpClientTransport( private val client: HttpClient, private val url: String, private val reconnectionOptions: ReconnectionOptions = ReconnectionOptions(), + private val maxInlineSseEventSize: Int = DEFAULT_MAX_INLINE_SSE_EVENT_SIZE, private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, ) : AbstractClientTransport() { + init { + require(maxInlineSseEventSize > 0) { "maxInlineSseEventSize must be greater than 0" } + } + @Deprecated( "Use constructor with ReconnectionOptions", replaceWith = ReplaceWith( @@ -98,7 +116,12 @@ public class StreamableHttpClientTransport( url: String, reconnectionTime: Duration?, requestBuilder: HttpRequestBuilder.() -> Unit = {}, - ) : this(client, url, ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), requestBuilder) + ) : this( + client, + url, + ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), + requestBuilder = requestBuilder, + ) override val logger: KLogger = KotlinLogging.logger {} @@ -458,7 +481,14 @@ public class StreamableHttpClientTransport( } while (!channel.isClosedForRead) { - val line = channel.readUTF8Line() ?: break + // Bound each line so a server that streams a line without ever terminating it cannot + // exhaust client memory; readUTF8Line returns null at the end of the stream. + val line = try { + channel.readUTF8Line(maxInlineSseEventSize) + } catch (_: TooLongLineException) { + throw TooLongFrameException(maxInlineSseEventSize.toLong() + 1, maxInlineSseEventSize) + } + if (line == null) break if (line.isEmpty()) { dispatch(id = id, eventName = eventName, data = sb.toString()) // reset @@ -472,7 +502,13 @@ public class StreamableHttpClientTransport( line.startsWith("event:") -> eventName = line.substringAfter("event:").trim() - line.startsWith("data:") -> sb.append(line.substringAfter("data:").trim()) + line.startsWith("data:") -> { + sb.append(line.substringAfter("data:").trim()) + // Cap an event assembled from many data: lines that never sees a terminating blank line. + if (sb.length > maxInlineSseEventSize) { + throw TooLongFrameException(sb.length.toLong(), maxInlineSseEventSize) + } + } line.startsWith("retry:") -> line.substringAfter("retry:").trim().toLongOrNull()?.let { localServerRetryDelay = it.milliseconds diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt index b380f64a6..ef5586754 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt @@ -19,6 +19,7 @@ import io.ktor.http.headersOf import io.ktor.utils.io.ByteReadChannel import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TooLongFrameException import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification @@ -43,11 +44,15 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class StreamableHttpClientTransportTest { - private fun createTransport(handler: MockRequestHandler): StreamableHttpClientTransport { + private fun createTransport( + maxInlineSseEventSize: Int = 16 * 1024 * 1024, + handler: MockRequestHandler, + ): StreamableHttpClientTransport { val mockEngine = MockEngine(handler) val httpClient = HttpClient(mockEngine) { install(SSE) { @@ -55,7 +60,11 @@ class StreamableHttpClientTransportTest { } } - return StreamableHttpClientTransport(httpClient, url = "http://localhost:8080/mcp") + return StreamableHttpClientTransport( + httpClient, + url = "http://localhost:8080/mcp", + maxInlineSseEventSize = maxInlineSseEventSize, + ) } private fun buildSseMessage(id: String, method: String, params: String): String = buildString { @@ -551,6 +560,97 @@ class StreamableHttpClientTransportTest { transport.close() } + @Test + fun testInlineSseRejectsEventExceedingMaxSize() = runTest { + // A malicious server streams an endless single event: many `data:` lines that accumulate + // past the cap and never send the blank-line terminator that would flush the buffer. + val maxInlineSseEventSize = 64 + val transport = createTransport(maxInlineSseEventSize) { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + appendLine("event: message") + // 20 lines × 16 chars = 320 chars of accumulated data, no terminating blank line. + repeat(20) { appendLine("data: ${"A".repeat(16)}") } + } + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Text.EventStream.toString()), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val receivedErrors = mutableListOf() + transport.onMessage { receivedMessages.add(it) } + transport.onError { receivedErrors.add(it) } + transport.start() + + val error = assertFailsWith { + transport.send(JSONRPCRequest(id = "req-1", method = "test", params = buildJsonObject { })) + } + + error.cause.shouldBeInstanceOf() + receivedErrors.filterIsInstance() shouldHaveSize 1 + receivedMessages shouldHaveSize 0 + transport.close() + } + + @Test + fun testInlineSseEventExactlyAtMaxSizeIsAccepted() = runTest { + // An event whose assembled data length equals the cap must still be accepted and dispatched: + // the guard rejects only sizes strictly greater than the cap (parity with ReadBuffer). + val part1 = """{"jsonrpc":"2.0",""" + val part2 = """"method":"notifications/tools/list_changed"}""" + val maxInlineSseEventSize = (part1 + part2).length + + val transport = createTransport(maxInlineSseEventSize) { request -> + if (request.method == HttpMethod.Post) { + val sseContent = buildString { + appendLine("event: message") + appendLine("data: $part1") + appendLine("data: $part2") + appendLine() + } + respond( + content = ByteReadChannel(sseContent), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Text.EventStream.toString()), + ) + } else { + respond("", HttpStatusCode.OK) + } + } + + val receivedMessages = mutableListOf() + val receivedErrors = mutableListOf() + val messageReceived = CompletableDeferred() + transport.onMessage { + receivedMessages.add(it) + if (!messageReceived.isCompleted) messageReceived.complete(Unit) + } + transport.onError { receivedErrors.add(it) } + transport.start() + + transport.send(JSONRPCRequest(id = "req-1", method = "test", params = buildJsonObject { })) + + eventually { messageReceived.await() } + + receivedMessages shouldHaveSize 1 + (receivedMessages[0] as JSONRPCNotification).method shouldBe "notifications/tools/list_changed" + receivedErrors shouldHaveSize 0 + transport.close() + } + + @Test + fun testNonPositiveMaxInlineSseEventSizeThrows() { + assertFailsWith { + createTransport(maxInlineSseEventSize = 0) { respond("", HttpStatusCode.OK) } + } + } + @Test fun testInlineSSEInResponse() = runTest { val transport = createTransport { request -> @@ -716,7 +816,7 @@ class StreamableHttpClientTransportTest { eventually { while (receivedMessages.isEmpty()) { - delay(10) + delay(10.milliseconds) } } From c8a1103b163509e6f58900ec3bd99307031faeef Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 23 Jun 2026 22:14:59 +0200 Subject: [PATCH 171/187] feat: sep-1036 add URL Mode Elicitation (#843) closes #410 ## How Has This Been Tested? integration and unit tests ## Breaking Changes none ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .../kotlin/sdk/client/ClientTest.kt | 187 ++++++++++++++++++ .../kotlin/sdk/server/ClientConnectionTest.kt | 2 + kotlin-sdk-client/api/kotlin-sdk-client.api | 1 + .../kotlin/sdk/client/Client.kt | 45 ++++- kotlin-sdk-core/api/kotlin-sdk-core.api | 40 +++- .../kotlin/sdk/shared/Protocol.kt | 2 +- .../kotlin/sdk/types/McpException.kt | 21 +- .../types/UrlElicitationRequiredException.kt | 65 ++++++ .../kotlin/sdk/types/capabilities.kt | 9 + .../kotlin/sdk/types/elicitation.kt | 5 + .../kotlin/sdk/types/jsonRpc.kt | 7 + .../kotlin/sdk/types/CapabilitiesTest.kt | 20 ++ .../kotlin/sdk/types/ElicitationTest.kt | 83 ++++++++ .../kotlin/sdk/server/ClientConnection.kt | 9 + .../kotlin/sdk/server/Server.kt | 4 + 15 files changed, 494 insertions(+), 6 deletions(-) create mode 100644 kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredException.kt diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index 5c00b235c..5dfcf61ef 100644 --- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -14,7 +14,10 @@ import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotificationParams import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest @@ -46,6 +49,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.Tool import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UrlElicitationRequiredException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel @@ -1251,6 +1255,189 @@ class ClientTest { client.close() } + // ── URL-mode elicitation (SEP-1036) ───────────────────────────────── + + @Test + fun `should handle URL mode elicitation end-to-end`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject), + ), + ), + ) + + val elicitationId = "550e8400-e29b-41d4-a716-446655440000" + val url = "https://oauth.example.com/authorize" + + client.setElicitationHandler { request -> + val params = assertIs(request.params) + assertEquals(elicitationId, params.elicitationId) + assertEquals(url, params.url) + ElicitResult(action = ElicitResult.Action.Accept) + } + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) + + val serverSessionResult = CompletableDeferred() + listOf( + launch { client.connect(clientTransport) }, + launch { serverSessionResult.complete(server.createSession(serverTransport)) }, + ).joinAll() + val serverSession = serverSessionResult.await() + + val result = serverSession.createElicitation( + message = "Authorize access to continue", + elicitationId = elicitationId, + url = url, + ) + + assertEquals(ElicitResult.Action.Accept, result.action) + assertNull(result.content) + + client.close() + } + + @Test + fun `should reject URL mode elicitation when client supports only form mode`() = runTest { + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept) + } + + val exception = assertFailsWith { + serverSession.createElicitation( + message = "Authorize", + elicitationId = "id-1", + url = "https://example.com/auth", + ) + } + assertTrue(exception.message!!.contains("elicitation.url")) + + client.close() + } + + @Test + fun `should deliver elicitation complete notification to client`() = runTest { + val received = CompletableDeferred() + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject), + ), + ), + ) + client.setElicitationCompleteHandler { received.complete(it) } + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities()), + ) + + val serverSessionResult = CompletableDeferred() + listOf( + launch { client.connect(clientTransport) }, + launch { serverSessionResult.complete(server.createSession(serverTransport)) }, + ).joinAll() + val serverSession = serverSessionResult.await() + + val elicitationId = "complete-id-1" + serverSession.sendElicitationComplete( + ElicitationCompleteNotification(ElicitationCompleteNotificationParams(elicitationId = elicitationId)), + ) + + val notification = received.await() + assertEquals(elicitationId, notification.params.elicitationId) + + client.close() + } + + @Test + fun `should reject elicitation complete when client supports only form mode`() = runTest { + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept) + } + + val exception = assertFailsWith { + serverSession.sendElicitationComplete( + ElicitationCompleteNotification(ElicitationCompleteNotificationParams(elicitationId = "id-1")), + ) + } + assertTrue(exception.message!!.contains("elicitation.url")) + + client.close() + } + + @Test + fun `setElicitationCompleteHandler should require url capability`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + elicitation = ClientCapabilities.Elicitation(), + ), + ), + ) + + assertFailsWith { + client.setElicitationCompleteHandler { } + } + } + + @Test + fun `should surface URL elicitation required error to client as typed exception`() = runTest { + val client = Client( + Implementation(name = "test client", version = "1.0"), + ClientOptions( + capabilities = ClientCapabilities( + elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject), + ), + ), + ) + + val elicitationId = "auth-required-1" + val url = "https://oauth.example.com/authorize" + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions(capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(true))), + ) + server.addTool("needs-auth", "Requires URL elicitation") { + throw UrlElicitationRequiredException( + listOf( + ElicitRequestURLParams( + message = "Authorize to continue", + elicitationId = elicitationId, + url = url, + ), + ), + ) + } + + val serverSessionResult = CompletableDeferred() + listOf( + launch { client.connect(clientTransport) }, + launch { serverSessionResult.complete(server.createSession(serverTransport)) }, + ).joinAll() + serverSessionResult.await() + + val exception = assertFailsWith { + client.callTool(name = "needs-auth", arguments = emptyMap()) + } + val elicitation = exception.elicitations.single() + assertEquals(elicitationId, elicitation.elicitationId) + assertEquals(url, elicitation.url) + + client.close() + } + private fun defaultsTestSchema(): ElicitRequestParams.RequestedSchema = ElicitRequestParams.RequestedSchema( properties = mapOf( "name" to StringSchema(description = "User name", default = "John Doe"), diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt index ee8953a7f..581e5ae94 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt @@ -9,6 +9,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest @@ -61,6 +62,7 @@ class ClientConnectionTest : AbstractServerFeaturesTest() { override fun getClientCapabilities(): ClientCapabilities = ClientCapabilities( roots = ClientCapabilities.Roots(listChanged = true), + elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject), ) private val sampleRoots = listOf(Root("file:///project", "Project Root")) diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index 0a544f510..1493a3029 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -34,6 +34,7 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextp 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 setElicitationCompleteHandler (Lkotlin/jvm/functions/Function1;)V public final fun setElicitationHandler (Lkotlin/jvm/functions/Function1;)V public final fun setLoggingLevel (Lio/modelcontextprotocol/kotlin/sdk/types/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/types/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index ee865afe5..b88cb1b68 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -19,6 +19,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult +import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult @@ -63,6 +64,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.TitledSingleSelectEnumSchema import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.supportsUrl import io.modelcontextprotocol.kotlin.sdk.types.toJson import kotlinx.atomicfu.atomic import kotlinx.atomicfu.getAndUpdate @@ -70,6 +72,7 @@ import kotlinx.atomicfu.update import kotlinx.collections.immutable.minus import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toPersistentSet +import kotlinx.coroutines.CompletableDeferred import kotlinx.serialization.SerializationException import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject @@ -643,9 +646,18 @@ public open class Client(private val clientInfo: Implementation, options: Client /** * Sets the elicitation handler. * - * When the handler returns [ElicitResult.Action.Accept], any properties missing from + * The handler receives both form-mode ([ElicitRequestFormParams]) and URL-mode + * ([io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams]) requests; + * branch on `request.params` to tell them apart. For URL mode, + * the host application must obtain explicit user consent and display the target domain before + * navigating — the SDK never opens or validates the URL — and should return + * [ElicitResult.Action.Decline] or [ElicitResult.Action.Cancel] when it cannot or will not proceed. + * A URL-mode [ElicitResult.Action.Accept] only signals consent; the outcome arrives out-of-band via + * [setElicitationCompleteHandler]. + * + * When a form-mode handler returns [ElicitResult.Action.Accept], any properties missing from * [ElicitResult.content] are automatically populated with default values defined in the - * elicitation schema. + * elicitation schema. URL-mode responses carry no content. * * @param handler The elicitation handler. * @throws IllegalStateException if the client does not support elicitation. @@ -663,6 +675,35 @@ public open class Client(private val clientInfo: Implementation, options: Client } } + /** + * Sets the handler invoked when the server reports that a URL-mode elicitation has completed. + * + * The handler is called for every `notifications/elicitation/complete` notification. Because the + * server only sends this for an out-of-band (URL-mode) interaction, the client must support url-mode + * elicitation. The client is responsible for correlating the notification's `elicitationId` with a + * pending elicitation, ignoring unknown or already-completed identifiers, and providing a manual way + * to continue if a notification never arrives. + * + * @param handler Invoked with each completion notification. + * @throws IllegalStateException if the client does not support url-mode elicitation. + */ + public fun setElicitationCompleteHandler(handler: (ElicitationCompleteNotification) -> Unit) { + check(capabilities.elicitation.supportsUrl) { + logger.error { + "Failed to set elicitation-complete handler: client does not support url-mode elicitation" + } + "Client does not support url-mode elicitation." + } + logger.info { "Setting the elicitation-complete handler" } + + setNotificationHandler( + Method.Defined.NotificationsElicitationComplete, + ) { notification -> + handler(notification) + CompletableDeferred(Unit) + } + } + // --- Internal Handlers --- private fun applyElicitationDefaults(request: ElicitRequest, result: ElicitResult): ElicitResult { diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index 089572293..1904dc772 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -610,6 +610,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotificatio public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesKt { + public static final fun getSupportsUrl (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;)Z +} + public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Companion; public fun ()V @@ -3128,7 +3132,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Logging_dslKt { public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/types/McpDsl : java/lang/annotation/Annotation { } -public final class io/modelcontextprotocol/kotlin/sdk/types/McpException : java/lang/Exception { +public class io/modelcontextprotocol/kotlin/sdk/types/McpException : java/lang/Exception { public fun (I)V public fun (ILjava/lang/String;)V public fun (ILjava/lang/String;Lkotlinx/serialization/json/JsonElement;)V @@ -3744,6 +3748,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RPCError$ErrorCode { public static final field PARSE_ERROR I public static final field REQUEST_TIMEOUT I public static final field RESOURCE_NOT_FOUND I + public static final field URL_ELICITATION_REQUIRED I } public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { @@ -5814,6 +5819,39 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelect public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData { + public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData$Companion; + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData; + public fun equals (Ljava/lang/Object;)Z + public final fun getElicitations ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData; + 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/types/UrlElicitationRequiredData;)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/types/UrlElicitationRequiredData$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredException : io/modelcontextprotocol/kotlin/sdk/types/McpException { + public fun (Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getElicitations ()Ljava/util/List; +} + public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/WithMeta { public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/WithMeta$Companion; public abstract fun getMeta ()Lkotlinx/serialization/json/JsonObject; diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 8f86c035f..50dc3561c 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -393,7 +393,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio handler(response, null) } else { checkNotNull(error) - val mcpException = McpException( + val mcpException = McpException.fromError( code = error.error.code, message = error.error.message, data = error.error.data, diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt index 56c851bfb..6a9015bda 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt @@ -12,9 +12,26 @@ import kotlin.jvm.JvmOverloads * @property data optional additional error payload as a JSON element * @param cause the original cause */ -public class McpException @JvmOverloads public constructor( +public open class McpException @JvmOverloads public constructor( public val code: Int, message: String = "MCP error $code", public val data: JsonElement? = null, cause: Throwable? = null, -) : Exception(message, cause) +) : Exception(message, cause) { + internal companion object { + /** + * Reconstructs the most specific [McpException] subtype for a JSON-RPC error. + * + * Recognizes [RPCError.ErrorCode.URL_ELICITATION_REQUIRED] errors and returns a + * [UrlElicitationRequiredException] when [data] carries valid URL-mode elicitations; + * otherwise returns a plain [McpException]. Never throws — a malformed payload simply + * degrades to a plain [McpException]. + */ + fun fromError(code: Int, message: String, data: JsonElement?): McpException { + if (code == RPCError.ErrorCode.URL_ELICITATION_REQUIRED && data != null) { + UrlElicitationRequiredException.fromDataOrNull(message, data)?.let { return it } + } + return McpException(code = code, message = message, data = data) + } + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredException.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredException.kt new file mode 100644 index 000000000..f3da768b6 --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredException.kt @@ -0,0 +1,65 @@ +package io.modelcontextprotocol.kotlin.sdk.types + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement + +/** + * Payload carried by the [data][RPCError.data] of a [RPCError.ErrorCode.URL_ELICITATION_REQUIRED] error. + * + * @property elicitations The URL-mode elicitations that must be completed before the original request + * can succeed. Each entry is equivalent to an `elicitation/create` request. + */ +@Serializable +public data class UrlElicitationRequiredData(val elicitations: List) + +/** + * Signals that one or more URL-mode elicitations must be completed before a request can be processed. + * + * Serialized over the wire as a JSON-RPC error with code [RPCError.ErrorCode.URL_ELICITATION_REQUIRED] + * and [data][RPCError.data] holding the [elicitations]. A server throws this from a request handler to + * tell the client that a URL-mode elicitation is required; the client receives it from any request as a + * typed exception, equivalent to receiving an `elicitation/create` request. + * + * The client is responsible for obtaining explicit user consent and surfacing each [ElicitRequestURLParams] + * before navigation — the SDK never opens or validates URLs. + * + * Example client-side handling: + * ```kotlin + * try { + * client.callTool(name = "create_repo", arguments = args) + * } catch (e: UrlElicitationRequiredException) { + * for (elicitation in e.elicitations) { + * // Show elicitation.message and the target domain, then ask the user for consent. + * if (userConsents(elicitation.url)) openInBrowser(elicitation.url) + * // Await completion via Client.setElicitationCompleteHandler, then optionally retry the call. + * } + * } + * ``` + * + * @property elicitations The required URL-mode elicitations (always non-empty). + */ +public class UrlElicitationRequiredException( + public val elicitations: List, + message: String = defaultMessage(elicitations), +) : McpException( + code = RPCError.ErrorCode.URL_ELICITATION_REQUIRED, + message = message, + data = McpJson.encodeToJsonElement(UrlElicitationRequiredData(elicitations)), +) { + internal companion object { + private fun defaultMessage(elicitations: List): String = + if (elicitations.size > 1) "URL elicitations are required" else "URL elicitation is required" + + /** + * Reconstructs a [UrlElicitationRequiredException] from JSON-RPC error [data], or `null` when the + * payload is absent, malformed, or carries no elicitations. Never throws. + */ + fun fromDataOrNull(message: String, data: JsonElement): UrlElicitationRequiredException? = + runCatching { McpJson.decodeFromJsonElement(data).elicitations } + .getOrNull() + ?.takeIf { it.isNotEmpty() } + ?.let { UrlElicitationRequiredException(it, message) } + } +} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt index cd78b582a..ecb8c4851 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt @@ -189,6 +189,15 @@ public data class ClientCapabilities( } } +/** + * Whether the client supports url-mode elicitation (out-of-band interaction via URL navigation). + * + * `false` when the client declared no elicitation capability at all, or only form mode (an empty + * [ClientCapabilities.Elicitation] is treated as form mode only). + */ +public val ClientCapabilities.Elicitation?.supportsUrl: Boolean + get() = this?.url != null + /** * Capabilities that a server may support. * diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt index a0ee74c9a..1bb73e6ea 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt @@ -133,6 +133,11 @@ public data class ElicitRequestFormParams( * Directs the user to an external URL for out-of-band interactions (e.g., OAuth flows, * payment processing, or entering sensitive credentials) that must not pass through the MCP client. * + * Handling the [url] is the responsibility of the host application: it should require HTTPS, clearly + * display the target domain, obtain explicit user consent, and open the URL in a secure browser context. + * The SDK neither opens, fetches, nor validates the URL. URLs must only appear in URL-mode requests — + * never render a URL from a form-mode request as a clickable link. + * * @property message The message explaining why the interaction is needed. * @property elicitationId A unique identifier for this elicitation. The client MUST treat * this ID as an opaque value. diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt index 7eefdb500..a9d465f67 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt @@ -269,6 +269,13 @@ public data class RPCError(val code: Int, val message: String, val data: JsonEle /** Resource not found */ public const val RESOURCE_NOT_FOUND: Int = -32002 + /** + * A URL-mode elicitation must be completed before the request can be processed. + * The error [data][RPCError.data] carries the required URL-mode elicitations + * (see [UrlElicitationRequiredException]). + */ + public const val URL_ELICITATION_REQUIRED: Int = -32042 + // Standard JSON-RPC 2.0 error codes /** Invalid JSON was received */ diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt index 06949a6a7..3468aae36 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt @@ -923,4 +923,24 @@ class CapabilitiesTest { assertEquals(EmptyJsonObject, capabilities.tasks?.requests?.sampling?.createMessage) assertEquals(EmptyJsonObject, capabilities.tasks?.requests?.elicitation?.create) } + + // ── Elicitation supportsUrl predicate ─────────────────────────────── + + @Test + fun `supportsUrl is false when no elicitation capability is declared`() { + val elicitation: ClientCapabilities.Elicitation? = null + elicitation.supportsUrl shouldBe false + } + + @Test + fun `supportsUrl is false for an empty form-only capability`() { + ClientCapabilities.Elicitation().supportsUrl shouldBe false + ClientCapabilities.Elicitation(form = EmptyJsonObject).supportsUrl shouldBe false + } + + @Test + fun `supportsUrl is true when url mode is declared`() { + ClientCapabilities.Elicitation(url = EmptyJsonObject).supportsUrl shouldBe true + ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject).supportsUrl shouldBe true + } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt index 1a418639a..bd5ef0626 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt @@ -10,6 +10,7 @@ import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put @@ -273,4 +274,86 @@ class ElicitationTest { ) } } + + // ── URLElicitationRequired error (-32042) ─────────────────────────── + + @Test + fun `UrlElicitationRequiredException carries code and serialized elicitations`() { + val elicitation = ElicitRequestURLParams( + message = "Authorize access to continue", + elicitationId = "550e8400-e29b-41d4-a716-446655440000", + url = "https://oauth.example.com/authorize", + ) + val exception = UrlElicitationRequiredException(listOf(elicitation)) + + exception.code shouldBe RPCError.ErrorCode.URL_ELICITATION_REQUIRED + exception.elicitations shouldBe listOf(elicitation) + + val data = exception.data.shouldNotBeNull() + McpJson.encodeToString(data) shouldEqualJson """ + { + "elicitations": [ + { + "message": "Authorize access to continue", + "elicitationId": "550e8400-e29b-41d4-a716-446655440000", + "url": "https://oauth.example.com/authorize", + "mode": "url" + } + ] + } + """.trimIndent() + } + + @Test + fun `fromError reconstructs typed exception for -32042 with valid data`() { + val data = McpJson.encodeToJsonElement( + UrlElicitationRequiredData( + listOf(ElicitRequestURLParams(message = "m", elicitationId = "id-1", url = "https://example.com/a")), + ), + ) + + val exception = McpException.fromError( + code = RPCError.ErrorCode.URL_ELICITATION_REQUIRED, + message = "needs auth", + data = data, + ) + + val typed = exception.shouldBeInstanceOf() + typed.message shouldBe "needs auth" + typed.elicitations.single().elicitationId shouldBe "id-1" + } + + @Test + fun `fromError returns plain McpException for unrelated code`() { + val exception = McpException.fromError( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "bad params", + data = null, + ) + (exception is UrlElicitationRequiredException) shouldBe false + exception.code shouldBe RPCError.ErrorCode.INVALID_PARAMS + } + + @Test + fun `fromError degrades to plain McpException for -32042 with malformed data`() { + val malformed = buildJsonObject { put("unexpected", "shape") } + + val exception = McpException.fromError( + code = RPCError.ErrorCode.URL_ELICITATION_REQUIRED, + message = "x", + data = malformed, + ) + + (exception is UrlElicitationRequiredException) shouldBe false + exception.code shouldBe RPCError.ErrorCode.URL_ELICITATION_REQUIRED + } + + @Test + fun `fromError degrades to plain McpException when elicitations empty`() { + val data = McpJson.encodeToJsonElement(UrlElicitationRequiredData(emptyList())) + + val exception = McpException.fromError(RPCError.ErrorCode.URL_ELICITATION_REQUIRED, "x", data) + + (exception is UrlElicitationRequiredException) shouldBe false + } } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt index 9b4b2f35b..343774be9 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt @@ -25,6 +25,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification import io.modelcontextprotocol.kotlin.sdk.types.ServerNotification import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.types.supportsUrl private val logger = KotlinLogging.logger {} @@ -238,6 +239,11 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client override suspend fun createElicitation(request: ElicitRequest, options: RequestOptions?): ElicitResult { val params = request.params + if (params is ElicitRequestURLParams) { + require(session.clientCapabilities?.elicitation.supportsUrl) { + "Client did not advertise elicitation.url capability; cannot send a URL-mode elicitation." + } + } logger.debug { when (params) { is ElicitRequestFormParams -> @@ -299,6 +305,9 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client } override suspend fun sendElicitationComplete(notification: ElicitationCompleteNotification) { + require(session.clientCapabilities?.elicitation.supportsUrl) { + "Client did not advertise elicitation.url capability; cannot send an elicitation completion notification." + } logger.debug { "Sending elicitation complete notification for: ${notification.params.elicitationId}" } notification(notification) } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index 1b7bfe7f0..c0d3e8493 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -46,6 +46,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ToolAnnotations import io.modelcontextprotocol.kotlin.sdk.types.ToolExecution import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest +import io.modelcontextprotocol.kotlin.sdk.types.UrlElicitationRequiredException import io.modelcontextprotocol.kotlin.sdk.utils.MatchResult import io.modelcontextprotocol.kotlin.sdk.utils.PathSegmentTemplateMatcher import io.modelcontextprotocol.kotlin.sdk.utils.ResourceTemplateMatcher @@ -651,6 +652,9 @@ public open class Server( } } catch (e: CancellationException) { throw e + } catch (e: UrlElicitationRequiredException) { + // Surface a required URL-mode elicitation as a JSON-RPC error (-32042) + throw e } catch (e: Exception) { logger.error(e) { "Error executing tool ${requestParams.name}" } CallToolResult( From c5abfe493a1967c7d09c8c7c238d6f6fab2ba691 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:31:48 +0000 Subject: [PATCH 172/187] chore(deps): bump @types/node from 25.9.3 to 26.0.0 in /integration-test/src/jvmTest/typescript in the all-dependencies group across 1 directory (#853) Bumps the all-dependencies group with 1 update in the /integration-test/src/jvmTest/typescript directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 25.9.3 to 26.0.0
    Commits

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/jvmTest/typescript/package-lock.json | 16 ++++++++-------- .../src/jvmTest/typescript/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index de65fb226..7e3293ce9 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -15,7 +15,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.9.3", + "@types/node": "^26.0.0", "cors": "^2.8.6", "tsx": "^4.22.4", "typescript": "^6.0.3" @@ -579,13 +579,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", - "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/@types/qs": { @@ -1727,9 +1727,9 @@ } }, "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index 629b3bb36..00f0961c5 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.9.3", + "@types/node": "^26.0.0", "cors": "^2.8.6", "tsx": "^4.22.4", "typescript": "^6.0.3" From bb265cc6e5df7d0ebf48dbdbc16a914046179da7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 23:09:37 +0200 Subject: [PATCH 173/187] chore(deps): bump hono from 4.12.23 to 4.12.27 in /integration-test/src/jvmTest/typescript (#837) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [hono](https://github.com/honojs/hono) from 4.12.23 to 4.12.27.
    Release notes

    Sourced from hono's releases.

    v4.12.27

    Security fixes

    This release includes fixes for the following security issues:

    hono/jsx does not isolate context per request

    Affects: hono/jsx, hono/jsx-renderer. During SSR, context was stored process-wide instead of per request, so useContext()/useRequestContext() read after an await in an async component could return another concurrent request's value — leading to cross-request data disclosure or authorization checks against the wrong request. GHSA-hvrm-45r6-mjfj

    Server-Side XSS via JSX escaping bypass in cx()

    Affects: hono/css. cx() marked its composed class name as already-escaped without escaping the input, so untrusted input passed as a class name could break out of the JSX class attribute during SSR and inject markup (XSS). GHSA-w62v-xxxg-mg59

    API Gateway v1 adapter can drop a repeated request header value

    Affects: hono/aws-lambda. The API Gateway v1 (and VPC Lattice) adapter de-duplicated repeated header values by substring instead of exact match, dropping a value that is a substring of another (e.g. 203.0.113.1 dropped when 203.0.113.10 is present) — affecting logic such as X-Forwarded-For-based IP restriction. GHSA-xgm2-5f3f-mvvc


    Users of hono/jsx/hono/jsx-renderer, hono/css (cx()), or the hono/aws-lambda API Gateway v1 / VPC Lattice adapters are encouraged to upgrade.

    v4.12.26

    What's Changed

    Full Changelog: https://github.com/honojs/hono/compare/v4.12.25...v4.12.26

    v4.12.25

    Security fixes

    This release includes fixes for the following security issues:

    CORS Middleware reflects any Origin with credentials when origin defaults to the wildcard

    Affects: hono/cors. Fixes the wildcard origin reflecting the request Origin and sending Access-Control-Allow-Credentials: true when credentials: true is set without an explicit origin, where any site a logged-in user visited could make credentialed cross-origin requests and read responses from cookie-authenticated endpoints. GHSA-88fw-hqm2-52qc

    Body Limit Middleware can be bypassed on AWS Lambda by understating Content-Length

    Affects: hono/body-limit on AWS Lambda (hono/aws-lambda, hono/lambda-edge). Fixes the request being built with the client-declared Content-Length while the body is delivered fully buffered, where a client could declare a small Content-Length with a much larger body and slip past the configured size limit. GHSA-rv63-4mwf-qqc2

    Path traversal in serve-static on Windows via encoded backslash (%5C)

    Affects: serveStatic on Windows (Node, Bun, Deno adapters). Fixes the path guard allowing a lone backslash, where an encoded backslash (%5C) decoded to \ was treated as a separator by the Windows path resolver, letting a single URL segment escape into a middleware-guarded subtree. GHSA-wwfh-h76j-fc44

    AWS Lambda adapter merges multiple Set-Cookie headers into one value, dropping cookies on ALB single-header and Lattice

    ... (truncated)

    Commits
    • 97c6fe1 4.12.27
    • aa92177 Merge commit from fork
    • cd3f6f7 Merge commit from fork
    • d4853a8 fix(jsx): make merged context-isolation tests pass tsc type check (#5037)
    • 6735fea fix(jsx): cast awaitedFallback through unknown to fix Deno type check (#5036)
    • fab3b13 Merge commit from fork
    • 9f0dadf ci: use npm Staged publishing (#5035)
    • 27b7992 4.12.26
    • d29982c chore: replace arg and glob with Bun native APIs in build script
    • 16215d5 chore: remove unused devcontainer and gitpod configs (#5029)
    • Additional commits viewable in compare view
    Maintainer changes

    This version was pushed to npm by GitHub Actions, a new releaser for hono since your current version.


    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 7e3293ce9..0fa51a9b6 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -1184,9 +1184,9 @@ } }, "node_modules/hono": { - "version": "4.12.23", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", - "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", "license": "MIT", "engines": { "node": ">=16.9.0" From 2edcb51d799f9c2a98082485dc189d457a35c27f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:31:46 +0200 Subject: [PATCH 174/187] chore(deps): bump the other-dependencies group across 5 directories with 3 updates (#854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the other-dependencies group with 2 updates in the /samples/kotlin-mcp-client directory: [gradle-wrapper](https://github.com/gradle/gradle) and [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java). Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-server directory: [gradle-wrapper](https://github.com/gradle/gradle). Bumps the other-dependencies group with 2 updates in the /samples/kotlinlang-mcp-server directory: [gradle-wrapper](https://github.com/gradle/gradle) and [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback). Bumps the other-dependencies group with 1 update in the /samples/simple-streamable-server directory: [gradle-wrapper](https://github.com/gradle/gradle). Bumps the other-dependencies group with 1 update in the /samples/weather-stdio-server directory: [gradle-wrapper](https://github.com/gradle/gradle). Updates `gradle-wrapper` from 9.5.1 to 9.6.0
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    9.6.0 RC3

    ... (truncated)

    Commits
    • 3f750f0 Update distro size for Gradle 9.6.0 release (#38243)
    • ae93cfa update distro size
    • f7e22b5 Update Gradle wrapper to version 9.6.0-rc-3 (#38227)
    • 71a8eb9 Update Gradle wrapper to version 9.6.0-rc-3
    • 70a8745 Prepare release notes for Gradle 9.6.0RC3 (#38220)
    • 9706522 some final polishing for release notes
    • af308eb Restore GradleInternal.getRootProject overload for binary compatibility (#38214)
    • 239361b Restore GradleInternal.getRootProject overload for binary compatibility
    • 896dc44 Update Gradle wrapper to version 9.6.0-rc-2 (#38187)
    • 2d4ec25 Update Gradle wrapper to version 9.6.0-rc-2
    • Additional commits viewable in compare view

    Updates `com.anthropic:anthropic-java` from 2.40.1 to 2.43.0
    Release notes

    Sourced from com.anthropic:anthropic-java's releases.

    v2.43.0

    2.43.0 (2026-06-18)

    Full Changelog: v2.42.0...v2.43.0

    Features

    • helpers: introduce x-stainless-helper telemetry + tag the refusal-fallback interceptor (#91) (a14925b)

    v2.42.0

    2.42.0 (2026-06-18)

    Full Changelog: v2.41.1...v2.42.0

    Features

    • api: add support for new code_execution_20260120 tool (1a1a6b9)

    v2.41.1

    2.41.1 (2026-06-18)

    Full Changelog: v2.41.0...v2.41.1

    Chores

    Build System

    v2.41.0

    2.41.0 (2026-06-17)

    Full Changelog: v2.40.1...v2.41.0

    Bug Fixes

    • client: bad model definitions (96787c6)
    • foundry: align baseUrl contract with the other SDKs (/anthropic in baseUrl, no path rewrite) (#84) (e651d93)

    Chores

    • api: remove retired models from API and SDKs (da1c93d)
    • upgrade all deps (#73) (e57f1b5)

    Styles

    ... (truncated)

    Changelog

    Sourced from com.anthropic:anthropic-java's changelog.

    2.43.0 (2026-06-18)

    Full Changelog: v2.42.0...v2.43.0

    Features

    • helpers: introduce x-stainless-helper telemetry + tag the refusal-fallback interceptor (#91) (a14925b)

    2.42.0 (2026-06-18)

    Full Changelog: v2.41.1...v2.42.0

    Features

    • api: add support for new code_execution_20260120 tool (1a1a6b9)

    2.41.1 (2026-06-18)

    Full Changelog: v2.41.0...v2.41.1

    Chores

    Build System

    2.41.0 (2026-06-17)

    Full Changelog: v2.40.1...v2.41.0

    Bug Fixes

    • client: bad model definitions (96787c6)
    • foundry: align baseUrl contract with the other SDKs (/anthropic in baseUrl, no path rewrite) (#84) (e651d93)

    Chores

    • api: remove retired models from API and SDKs (da1c93d)
    • upgrade all deps (#73) (e57f1b5)

    Styles

    ... (truncated)

    Commits
    • 8e217ec release: 2.43.0
    • 8f4d5be feat(helpers): introduce x-stainless-helper telemetry + tag the refusal-fallb...
    • 60e15dd release: 2.42.0
    • e430bfb feat(api): add support for new code_execution_20260120 tool
    • 3c27033 release: 2.41.1
    • 1d77d61 build: fix signing for publishing (#90)
    • 78e23a5 chore: fix release runs-on (#354)
    • 946fe2a release: 2.41.0
    • 536e177 style: apply formatting (#88)
    • f719e12 fix(client): bad model definitions
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.1 to 9.6.0
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    9.6.0 RC3

    ... (truncated)

    Commits
    • 3f750f0 Update distro size for Gradle 9.6.0 release (#38243)
    • ae93cfa update distro size
    • f7e22b5 Update Gradle wrapper to version 9.6.0-rc-3 (#38227)
    • 71a8eb9 Update Gradle wrapper to version 9.6.0-rc-3
    • 70a8745 Prepare release notes for Gradle 9.6.0RC3 (#38220)
    • 9706522 some final polishing for release notes
    • af308eb Restore GradleInternal.getRootProject overload for binary compatibility (#38214)
    • 239361b Restore GradleInternal.getRootProject overload for binary compatibility
    • 896dc44 Update Gradle wrapper to version 9.6.0-rc-2 (#38187)
    • 2d4ec25 Update Gradle wrapper to version 9.6.0-rc-2
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.1 to 9.6.0
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    9.6.0 RC3

    ... (truncated)

    Commits
    • 3f750f0 Update distro size for Gradle 9.6.0 release (#38243)
    • ae93cfa update distro size
    • f7e22b5 Update Gradle wrapper to version 9.6.0-rc-3 (#38227)
    • 71a8eb9 Update Gradle wrapper to version 9.6.0-rc-3
    • 70a8745 Prepare release notes for Gradle 9.6.0RC3 (#38220)
    • 9706522 some final polishing for release notes
    • af308eb Restore GradleInternal.getRootProject overload for binary compatibility (#38214)
    • 239361b Restore GradleInternal.getRootProject overload for binary compatibility
    • 896dc44 Update Gradle wrapper to version 9.6.0-rc-2 (#38187)
    • 2d4ec25 Update Gradle wrapper to version 9.6.0-rc-2
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.1 to 9.6.0
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    9.6.0 RC3

    ... (truncated)

    Commits
    • 3f750f0 Update distro size for Gradle 9.6.0 release (#38243)
    • ae93cfa update distro size
    • f7e22b5 Update Gradle wrapper to version 9.6.0-rc-3 (#38227)
    • 71a8eb9 Update Gradle wrapper to version 9.6.0-rc-3
    • 70a8745 Prepare release notes for Gradle 9.6.0RC3 (#38220)
    • 9706522 some final polishing for release notes
    • af308eb Restore GradleInternal.getRootProject overload for binary compatibility (#38214)
    • 239361b Restore GradleInternal.getRootProject overload for binary compatibility
    • 896dc44 Update Gradle wrapper to version 9.6.0-rc-2 (#38187)
    • 2d4ec25 Update Gradle wrapper to version 9.6.0-rc-2
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.1 to 9.6.0
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    9.6.0 RC3

    ... (truncated)

    Commits
    • 3f750f0 Update distro size for Gradle 9.6.0 release (#38243)
    • ae93cfa update distro size
    • f7e22b5 Update Gradle wrapper to version 9.6.0-rc-3 (#38227)
    • 71a8eb9 Update Gradle wrapper to version 9.6.0-rc-3
    • 70a8745 Prepare release notes for Gradle 9.6.0RC3 (#38220)
    • 9706522 some final polishing for release notes
    • af308eb Restore GradleInternal.getRootProject overload for binary compatibility (#38214)
    • 239361b Restore GradleInternal.getRootProject overload for binary compatibility
    • 896dc44 Update Gradle wrapper to version 9.6.0-rc-2 (#38187)
    • 2d4ec25 Update Gradle wrapper to version 9.6.0-rc-2
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.1 to 9.6.0
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    9.6.0 RC3

    ... (truncated)

    Commits
    • 3f750f0 Update distro size for Gradle 9.6.0 release (#38243)
    • ae93cfa update distro size
    • f7e22b5 Update Gradle wrapper to version 9.6.0-rc-3 (#38227)
    • 71a8eb9 Update Gradle wrapper to version 9.6.0-rc-3
    • 70a8745 Prepare release notes for Gradle 9.6.0RC3 (#38220)
    • 9706522 some final polishing for release notes
    • af308eb Restore GradleInternal.getRootProject overload for binary compatibility (#38214)
    • 239361b Restore GradleInternal.getRootProject overload for binary compatibility
    • 896dc44 Update Gradle wrapper to version 9.6.0-rc-2 (#38187)
    • 2d4ec25 Update Gradle wrapper to version 9.6.0-rc-2
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.1 to 9.6.0
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    9.6.0 RC3

    ... (truncated)

    Commits
    • 3f750f0 Update distro size for Gradle 9.6.0 release (#38243)
    • ae93cfa update distro size
    • f7e22b5 Update Gradle wrapper to version 9.6.0-rc-3 (#38227)
    • 71a8eb9 Update Gradle wrapper to version 9.6.0-rc-3
    • 70a8745 Prepare release notes for Gradle 9.6.0RC3 (#38220)
    • 9706522 some final polishing for release notes
    • af308eb Restore GradleInternal.getRootProject overload for binary compatibility (#38214)
    • 239361b Restore GradleInternal.getRootProject overload for binary compatibility
    • 896dc44 Update Gradle wrapper to version 9.6.0-rc-2 (#38187)
    • 2d4ec25 Update Gradle wrapper to version 9.6.0-rc-2
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.1 to 9.6.0
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    9.6.0 RC3

    ... (truncated)

    Commits
    • 3f750f0 Update distro size for Gradle 9.6.0 release (#38243)
    • ae93cfa update distro size
    • f7e22b5 Update Gradle wrapper to version 9.6.0-rc-3 (#38227)
    • 71a8eb9 Update Gradle wrapper to version 9.6.0-rc-3
    • 70a8745 Prepare release notes for Gradle 9.6.0RC3 (#38220)
    • 9706522 some final polishing for release notes
    • af308eb Restore GradleInternal.getRootProject overload for binary compatibility (#38214)
    • 239361b Restore GradleInternal.getRootProject overload for binary compatibility
    • 896dc44 Update Gradle wrapper to version 9.6.0-rc-2 (#38187)
    • 2d4ec25 Update Gradle wrapper to version 9.6.0-rc-2
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.1 to 9.6.0
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    9.6.0 RC3

    ... (truncated)

    Commits
    • 3f750f0 Update distro size for Gradle 9.6.0 release (#38243)
    • ae93cfa update distro size
    • f7e22b5 Update Gradle wrapper to version 9.6.0-rc-3 (#38227)
    • 71a8eb9 Update Gradle wrapper to version 9.6.0-rc-3
    • 70a8745 Prepare release notes for Gradle 9.6.0RC3 (#38220)
    • 9706522 some final polishing for release notes
    • af308eb Restore GradleInternal.getRootProject overload for binary compatibility (#38214)
    • 239361b Restore GradleInternal.getRootProject overload for binary compatibility
    • 896dc44 Update Gradle wrapper to version 9.6.0-rc-2 (#38187)
    • 2d4ec25 Update Gradle wrapper to version 9.6.0-rc-2
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.1 to 9.6.0
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    9.6.0 RC3

    ... (truncated)

    Commits
    • 3f750f0 Update distro size for Gradle 9.6.0 release (#38243)
    • ae93cfa update distro size
    • f7e22b5 Update Gradle wrapper to version 9.6.0-rc-3 (#38227)
    • 71a8eb9 Update Gradle wrapper to version 9.6.0-rc-3
    • 70a8745 Prepare release notes for Gradle 9.6.0RC3 (#38220)
    • 9706522 some final polishing for release notes
    • af308eb Restore GradleInternal.getRootProject overload for binary compatibility (#38214)
    • 239361b Restore GradleInternal.getRootProject overload for binary compatibility
    • 896dc44 Update Gradle wrapper to version 9.6.0-rc-2 (#38187)
    • 2d4ec25 Update Gradle wrapper to version 9.6.0-rc-2
    • Additional commits viewable in compare view

    Updates `gradle-wrapper` from 9.5.1 to 9.6.0
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking ... _Description has been truncated_ Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/kotlin-mcp-client/gradlew | 4 ++-- samples/kotlin-mcp-client/gradlew.bat | 4 ++-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/kotlin-mcp-server/gradlew | 4 ++-- samples/kotlin-mcp-server/gradlew.bat | 4 ++-- samples/kotlinlang-mcp-server/gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/kotlinlang-mcp-server/gradlew | 4 ++-- samples/kotlinlang-mcp-server/gradlew.bat | 4 ++-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/simple-streamable-server/gradlew | 4 ++-- samples/simple-streamable-server/gradlew.bat | 4 ++-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/weather-stdio-server/gradlew | 4 ++-- samples/weather-stdio-server/gradlew.bat | 4 ++-- 17 files changed, 27 insertions(+), 27 deletions(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 033fbdc20..747918697 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -anthropic = "2.40.1" +anthropic = "2.43.0" kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.13.0" diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties index df6a6ad76..eb84db68d 100644 --- a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/kotlin-mcp-client/gradlew b/samples/kotlin-mcp-client/gradlew index b9bb139f7..249efbb03 100755 --- a/samples/kotlin-mcp-client/gradlew +++ b/samples/kotlin-mcp-client/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: diff --git a/samples/kotlin-mcp-client/gradlew.bat b/samples/kotlin-mcp-client/gradlew.bat index aa5f10b06..8508ef684 100644 --- a/samples/kotlin-mcp-client/gradlew.bat +++ b/samples/kotlin-mcp-client/gradlew.bat @@ -19,7 +19,7 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## @@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2 -@rem Execute Gradle +@rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties index df6a6ad76..eb84db68d 100644 --- a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/kotlin-mcp-server/gradlew b/samples/kotlin-mcp-server/gradlew index b9bb139f7..249efbb03 100755 --- a/samples/kotlin-mcp-server/gradlew +++ b/samples/kotlin-mcp-server/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: diff --git a/samples/kotlin-mcp-server/gradlew.bat b/samples/kotlin-mcp-server/gradlew.bat index aa5f10b06..8508ef684 100644 --- a/samples/kotlin-mcp-server/gradlew.bat +++ b/samples/kotlin-mcp-server/gradlew.bat @@ -19,7 +19,7 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## @@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2 -@rem Execute Gradle +@rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml index fdef6d30b..69dfe7e9b 100644 --- a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -4,7 +4,7 @@ ktor = "3.3.3" mcp-kotlin = "0.13.0" serialization = "1.11.0" coroutines = "1.11.0" -logback = "1.5.34" +logback = "1.5.35" shadow = "9.4.2" [libraries] diff --git a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties index df6a6ad76..eb84db68d 100644 --- a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/kotlinlang-mcp-server/gradlew b/samples/kotlinlang-mcp-server/gradlew index b9bb139f7..249efbb03 100755 --- a/samples/kotlinlang-mcp-server/gradlew +++ b/samples/kotlinlang-mcp-server/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: diff --git a/samples/kotlinlang-mcp-server/gradlew.bat b/samples/kotlinlang-mcp-server/gradlew.bat index aa5f10b06..8508ef684 100644 --- a/samples/kotlinlang-mcp-server/gradlew.bat +++ b/samples/kotlinlang-mcp-server/gradlew.bat @@ -19,7 +19,7 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## @@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2 -@rem Execute Gradle +@rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel diff --git a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties index df6a6ad76..eb84db68d 100644 --- a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/simple-streamable-server/gradlew b/samples/simple-streamable-server/gradlew index b9bb139f7..249efbb03 100755 --- a/samples/simple-streamable-server/gradlew +++ b/samples/simple-streamable-server/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: diff --git a/samples/simple-streamable-server/gradlew.bat b/samples/simple-streamable-server/gradlew.bat index aa5f10b06..8508ef684 100644 --- a/samples/simple-streamable-server/gradlew.bat +++ b/samples/simple-streamable-server/gradlew.bat @@ -19,7 +19,7 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## @@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2 -@rem Execute Gradle +@rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties index df6a6ad76..eb84db68d 100644 --- a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/weather-stdio-server/gradlew b/samples/weather-stdio-server/gradlew index b9bb139f7..249efbb03 100755 --- a/samples/weather-stdio-server/gradlew +++ b/samples/weather-stdio-server/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: diff --git a/samples/weather-stdio-server/gradlew.bat b/samples/weather-stdio-server/gradlew.bat index aa5f10b06..8508ef684 100644 --- a/samples/weather-stdio-server/gradlew.bat +++ b/samples/weather-stdio-server/gradlew.bat @@ -19,7 +19,7 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## @@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2 -@rem Execute Gradle +@rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel From a84ff59b53c26feec583cbfffa2e61d50e92c8a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:51:56 +0000 Subject: [PATCH 175/187] chore(deps): bump actions/checkout from 6 to 7 in the all-actions group across 1 directory (#852) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-actions group with 1 update in the / directory: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6 to 7

    Release notes

    Sourced from actions/checkout's releases.

    v7.0.0

    What's Changed

    New Contributors

    Full Changelog: https://github.com/actions/checkout/compare/v6.0.3...v7.0.0

    v6.0.3

    What's Changed

    New Contributors

    Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.3

    v6.0.2

    What's Changed

    Full Changelog: https://github.com/actions/checkout/compare/v6.0.1...v6.0.2

    v6.0.1

    What's Changed

    Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.1

    Changelog

    Sourced from actions/checkout's changelog.

    Changelog

    v7.0.0

    v6.0.3

    v6.0.2

    v6.0.1

    v6.0.0

    v5.0.1

    v5.0.0

    v4.3.1

    v4.3.0

    v4.2.2

    v4.2.1

    ... (truncated)

    Commits

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/apidocs.yaml | 2 +- .github/workflows/build.yml | 6 +++--- .github/workflows/codeql.yml | 2 +- .github/workflows/conformance.yml | 8 ++++---- .github/workflows/gradle-publish.yml | 2 +- .github/workflows/samples.yml | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/apidocs.yaml b/.github/workflows/apidocs.yaml index 995f7d8bb..510af198d 100644 --- a/.github/workflows/apidocs.yaml +++ b/.github/workflows/apidocs.yaml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v5 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad4cc7ae0..2b6e415a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: max-workers: 2 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v5 @@ -126,7 +126,7 @@ jobs: contents: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v5 @@ -168,7 +168,7 @@ jobs: name: "Build Sample: ${{ matrix.sample }}" timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v5 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5697b61b5..f25ec6451 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v5 diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 4058d02cc..2511286d7 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -25,7 +25,7 @@ jobs: timeout-minutes: 20 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK uses: actions/setup-java@v5 @@ -76,7 +76,7 @@ jobs: - sse-retry steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK uses: actions/setup-java@v5 @@ -107,7 +107,7 @@ jobs: timeout-minutes: 20 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK uses: actions/setup-java@v5 @@ -146,7 +146,7 @@ jobs: - auth/cross-app-access-complete-flow steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK uses: actions/setup-java@v5 diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index 6ed075651..f9df05394 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -17,7 +17,7 @@ jobs: packages: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK 21 uses: actions/setup-java@v5 with: diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 2c910fddb..89de69940 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -36,7 +36,7 @@ jobs: name: Build Sample (${{ matrix.sample }}) timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v5 From 3274244f7d018d4108d8611c95eb10cc7dbc18ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:27:25 +0000 Subject: [PATCH 176/187] chore(deps): bump gradle-wrapper from 9.5.1 to 9.6.0 (#851) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [gradle-wrapper](https://github.com/gradle/gradle) from 9.5.1 to 9.6.0.
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.0

    The Gradle team is excited to announce Gradle 9.6.0.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.0 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.0 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    9.6.0 RC3

    ... (truncated)

    Commits
    • 3f750f0 Update distro size for Gradle 9.6.0 release (#38243)
    • ae93cfa update distro size
    • f7e22b5 Update Gradle wrapper to version 9.6.0-rc-3 (#38227)
    • 71a8eb9 Update Gradle wrapper to version 9.6.0-rc-3
    • 70a8745 Prepare release notes for Gradle 9.6.0RC3 (#38220)
    • 9706522 some final polishing for release notes
    • af308eb Restore GradleInternal.getRootProject overload for binary compatibility (#38214)
    • 239361b Restore GradleInternal.getRootProject overload for binary compatibility
    • 896dc44 Update Gradle wrapper to version 9.6.0-rc-2 (#38187)
    • 2d4ec25 Update Gradle wrapper to version 9.6.0-rc-2
    • Additional commits viewable in compare view

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 4 ++-- gradlew.bat | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df6a6ad76..eb84db68d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/gradlew b/gradlew index b9bb139f7..249efbb03 100755 --- a/gradlew +++ b/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: diff --git a/gradlew.bat b/gradlew.bat index aa5f10b06..8508ef684 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,7 +19,7 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## @@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2 -@rem Execute Gradle +@rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel From 2967696e6ca32dcaddb88fe890a2127010e07edb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:49:27 +0000 Subject: [PATCH 177/187] chore(deps): bump com.vanniktech:gradle-maven-publish-plugin from 0.36.0 to 0.37.0 in the gradle-plugins group across 1 directory (#848) Bumps the gradle-plugins group with 1 update in the / directory: [com.vanniktech:gradle-maven-publish-plugin](https://github.com/vanniktech/gradle-maven-publish-plugin). Updates `com.vanniktech:gradle-maven-publish-plugin` from 0.36.0 to 0.37.0
    Release notes

    Sourced from com.vanniktech:gradle-maven-publish-plugin's releases.

    0.37.0

    • When publishing to Maven Central, redundant checksum files are now excluded by default: checksums of .asc signature files (gradle/gradle#20232) and the sha256/sha512 checksums, which are never read by Gradle or Maven Central. The published checksums can be configured through checksums(...) in the DSL or the mavenCentralChecksums Gradle property (default md5,sha1). Signature checksum exclusion can be controlled through excludeSignatureChecksums() or the mavenCentralExcludeSignatureChecksums Gradle property.
    • Maven Central deployment id is being logged after upload.

    Minimum supported versions

    • JDK 17
    • Gradle 9.0.0
    • Android Gradle Plugin 8.13.0
    • Kotlin Gradle Plugin 2.2.0

    Compatibility tested up to

    • JDK 26
    • Gradle 9.6.0
    • Gradle 9.7.0-milestone-1
    • Android Gradle Plugin 9.2.1
    • Android Gradle Plugin 9.3.0-rc01
    • Android Gradle Plugin 9.4.0-alpha01
    • Kotlin Gradle Plugin 2.4.0

    0.37.0-rc1

    • When publishing to Maven Central, redundant checksum files are now excluded by default: checksums of .asc signature files (gradle/gradle#20232) and the sha256/sha512 checksums, which are never read by Gradle or Maven Central. The published checksums can be configured through checksums(...) in the DSL or the mavenCentralChecksums Gradle property (default md5,sha1). Signature checksum exclusion can be controlled through excludeSignatureChecksums() or the mavenCentralExcludeSignatureChecksums Gradle property.
    • Maven Central deployment id is being logged after upload.

    Minimum supported versions

    • JDK 17
    • Gradle 9.0.0
    • Android Gradle Plugin 8.13.0
    • Kotlin Gradle Plugin 2.2.0

    Compatibility tested up to

    • JDK 26
    • Gradle 9.6.0
    • Gradle 9.7.0-milestone-1
    • Android Gradle Plugin 9.2.1
    • Android Gradle Plugin 9.3.0-rc01
    • Android Gradle Plugin 9.4.0-alpha01
    • Kotlin Gradle Plugin 2.4.0
    Changelog

    Sourced from com.vanniktech:gradle-maven-publish-plugin's changelog.

    0.37.0 (2026-06-21)

    • When publishing to Maven Central, redundant checksum files are now excluded by default: checksums of .asc signature files (gradle/gradle#20232) and the sha256/sha512 checksums, which are never read by Gradle or Maven Central. The published checksums can be configured through checksums(...) in the DSL or the mavenCentralChecksums Gradle property (default md5,sha1). Signature checksum exclusion can be controlled through excludeSignatureChecksums() or the mavenCentralExcludeSignatureChecksums Gradle property.
    • Maven Central deployment id is being logged after upload.

    Minimum supported versions

    • JDK 17
    • Gradle 9.0.0
    • Android Gradle Plugin 8.13.0
    • Kotlin Gradle Plugin 2.2.0

    Compatibility tested up to

    • JDK 26
    • Gradle 9.6.0
    • Gradle 9.7.0-milestone-1
    • Android Gradle Plugin 9.2.1
    • Android Gradle Plugin 9.3.0-rc01
    • Android Gradle Plugin 9.4.0-alpha01
    • Kotlin Gradle Plugin 2.4.0
    Commits

    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 4904ea4b3..0b2929d13 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ kotlin = "2.3.21" kover = "0.9.8" ktlint = "14.2.0" knit = "0.5.1" -mavenPublish = "0.36.0" +mavenPublish = "0.37.0" netty = "4.2.15.Final" # libraries version From cca883aa3ff9c4c700287585c8e5db13d599d14d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 00:43:56 +0200 Subject: [PATCH 178/187] chore(deps): bump the kotest group across 1 directory with 3 updates (#847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the kotest group with 3 updates in the / directory: [io.kotest:kotest-assertions-core](https://github.com/kotest/kotest), [io.kotest:kotest-assertions-json](https://github.com/kotest/kotest) and [io.kotest:kotest-assertions-ktor](https://github.com/kotest/kotest). Updates `io.kotest:kotest-assertions-core` from 6.1.11 to 6.2.1
    Release notes

    Sourced from io.kotest:kotest-assertions-core's releases.

    v6.2.1

    What's Changed

    Full Changelog: https://github.com/kotest/kotest/compare/v6.2.0...v6.2.1

    v6.2.0

    ✨ Features

    New matchers / assertions

    comparables.shouldBeAtLeast — @​AlexCue987 (#5831) MultipleMatcherResult for combining multiple matcher results — @​sksamuel (#5723) shouldContainExactCopies for lists — @​AlexCue987 (#5913) Character matchers — @​JordanLongstaff (#5921) shouldBeSingle matcher — @​sksamuel (#6028) shouldContainRepeats for strings — @​AlexCue987 (#6004) Infix form for inspectors — @​alfonsoristorato (#5920) Block-asserting overload for shouldBeFailure — @​alfonsoristorato (#5927) Per-call Eq overrides via a withEqs DSL — @​PreAgile (#6010) Per-element data class diffs in collection comparisons — @​PreAgile (#5835)

    Framework / API

    Run data tests singularly — @​alfonsoristorato (#5574) New JVM test-suites module (deprecates JunitXmlReporter) — @​sksamuel (#5801) Support isolation modes on all KMP platforms — @​sksamuel (#5806) Breadcrumbs support for Kotest spec files — @​sksamuel (#5793) Type-safe test metadata API — @​PreAgile (#5905) Public API for creating custom styles — @​sksamuel (#5814) Support multiple --test args joined by semicolon in KOTEST_INCLUDE_PATTERN — @​sksamuel (#5922) Extend life of deprecated Test containers and add onStart — @​sksamuel (#5896) Make Arb.stringPattern multiplatform — @​wilmveel (#6008) Complete the permutations module — @​sksamuel (#6016) Add Wasm WASI target to kotest-property and kotest-property-permutations — @​jsoizo (#6129) Make fibonacci interval functions public — @​mvanhorn (#6135) Add Android instrumented test module — @​sksamuel (#5900)

    IntelliJ plugin

    N-times setter for test invocation — @​alfonsoristorato (#5815) Invocation count on the Kotest producer — @​alfonsoristorato (#5837) Singular data-test run on the Kotest producer — @​alfonsoristorato (#5843) Amper-aware run-configuration producer — @​sksamuel (#5999)

    ... (truncated)

    Commits
    • 9ae8a1d Make flaky "throws FileNotFoundException for some time" eventually test deter...
    • a16146f Fix data objects being considered equal in DataClassEq (#6144) (#6145)
    • 47fe8d4 remove KotestInternal in TestFactory (#6146)
    • b39700c fix docs (#6147)
    • 8006dbd Remove usages of K1's KtUltraLightClass from kotest plugin sources (#6143)
    • 07b21d7 Add support for deprecated Kotlin targets in Android and Native conve… (#6141)
    • 96f71e0 docs: add KDoc to ArrayEq (#6140)
    • 7f2b14a Cut docs for 6.2
    • ead01c0 Override additional Kotlin version properties for IC-261 in release workflow
    • f3134ea align compile toolchain with minimum supported Kotlin version for KLIB ABI co...
    • Additional commits viewable in compare view

    Updates `io.kotest:kotest-assertions-json` from 6.1.11 to 6.2.1
    Release notes

    Sourced from io.kotest:kotest-assertions-json's releases.

    v6.2.1

    What's Changed

    Full Changelog: https://github.com/kotest/kotest/compare/v6.2.0...v6.2.1

    v6.2.0

    ✨ Features

    New matchers / assertions

    comparables.shouldBeAtLeast — @​AlexCue987 (#5831) MultipleMatcherResult for combining multiple matcher results — @​sksamuel (#5723) shouldContainExactCopies for lists — @​AlexCue987 (#5913) Character matchers — @​JordanLongstaff (#5921) shouldBeSingle matcher — @​sksamuel (#6028) shouldContainRepeats for strings — @​AlexCue987 (#6004) Infix form for inspectors — @​alfonsoristorato (#5920) Block-asserting overload for shouldBeFailure — @​alfonsoristorato (#5927) Per-call Eq overrides via a withEqs DSL — @​PreAgile (#6010) Per-element data class diffs in collection comparisons — @​PreAgile (#5835)

    Framework / API

    Run data tests singularly — @​alfonsoristorato (#5574) New JVM test-suites module (deprecates JunitXmlReporter) — @​sksamuel (#5801) Support isolation modes on all KMP platforms — @​sksamuel (#5806) Breadcrumbs support for Kotest spec files — @​sksamuel (#5793) Type-safe test metadata API — @​PreAgile (#5905) Public API for creating custom styles — @​sksamuel (#5814) Support multiple --test args joined by semicolon in KOTEST_INCLUDE_PATTERN — @​sksamuel (#5922) Extend life of deprecated Test containers and add onStart — @​sksamuel (#5896) Make Arb.stringPattern multiplatform — @​wilmveel (#6008) Complete the permutations module — @​sksamuel (#6016) Add Wasm WASI target to kotest-property and kotest-property-permutations — @​jsoizo (#6129) Make fibonacci interval functions public — @​mvanhorn (#6135) Add Android instrumented test module — @​sksamuel (#5900)

    IntelliJ plugin

    N-times setter for test invocation — @​alfonsoristorato (#5815) Invocation count on the Kotest producer — @​alfonsoristorato (#5837) Singular data-test run on the Kotest producer — @​alfonsoristorato (#5843) Amper-aware run-configuration producer — @​sksamuel (#5999)

    ... (truncated)

    Commits
    • 9ae8a1d Make flaky "throws FileNotFoundException for some time" eventually test deter...
    • a16146f Fix data objects being considered equal in DataClassEq (#6144) (#6145)
    • 47fe8d4 remove KotestInternal in TestFactory (#6146)
    • b39700c fix docs (#6147)
    • 8006dbd Remove usages of K1's KtUltraLightClass from kotest plugin sources (#6143)
    • 07b21d7 Add support for deprecated Kotlin targets in Android and Native conve… (#6141)
    • 96f71e0 docs: add KDoc to ArrayEq (#6140)
    • 7f2b14a Cut docs for 6.2
    • ead01c0 Override additional Kotlin version properties for IC-261 in release workflow
    • f3134ea align compile toolchain with minimum supported Kotlin version for KLIB ABI co...
    • Additional commits viewable in compare view

    Updates `io.kotest:kotest-assertions-ktor` from 6.1.11 to 6.2.1
    Release notes

    Sourced from io.kotest:kotest-assertions-ktor's releases.

    v6.2.1

    What's Changed

    Full Changelog: https://github.com/kotest/kotest/compare/v6.2.0...v6.2.1

    v6.2.0

    ✨ Features

    New matchers / assertions

    comparables.shouldBeAtLeast — @​AlexCue987 (#5831) MultipleMatcherResult for combining multiple matcher results — @​sksamuel (#5723) shouldContainExactCopies for lists — @​AlexCue987 (#5913) Character matchers — @​JordanLongstaff (#5921) shouldBeSingle matcher — @​sksamuel (#6028) shouldContainRepeats for strings — @​AlexCue987 (#6004) Infix form for inspectors — @​alfonsoristorato (#5920) Block-asserting overload for shouldBeFailure — @​alfonsoristorato (#5927) Per-call Eq overrides via a withEqs DSL — @​PreAgile (#6010) Per-element data class diffs in collection comparisons — @​PreAgile (#5835)

    Framework / API

    Run data tests singularly — @​alfonsoristorato (#5574) New JVM test-suites module (deprecates JunitXmlReporter) — @​sksamuel (#5801) Support isolation modes on all KMP platforms — @​sksamuel (#5806) Breadcrumbs support for Kotest spec files — @​sksamuel (#5793) Type-safe test metadata API — @​PreAgile (#5905) Public API for creating custom styles — @​sksamuel (#5814) Support multiple --test args joined by semicolon in KOTEST_INCLUDE_PATTERN — @​sksamuel (#5922) Extend life of deprecated Test containers and add onStart — @​sksamuel (#5896) Make Arb.stringPattern multiplatform — @​wilmveel (#6008) Complete the permutations module — @​sksamuel (#6016) Add Wasm WASI target to kotest-property and kotest-property-permutations — @​jsoizo (#6129) Make fibonacci interval functions public — @​mvanhorn (#6135) Add Android instrumented test module — @​sksamuel (#5900)

    IntelliJ plugin

    N-times setter for test invocation — @​alfonsoristorato (#5815) Invocation count on the Kotest producer — @​alfonsoristorato (#5837) Singular data-test run on the Kotest producer — @​alfonsoristorato (#5843) Amper-aware run-configuration producer — @​sksamuel (#5999)

    ... (truncated)

    Commits
    • 9ae8a1d Make flaky "throws FileNotFoundException for some time" eventually test deter...
    • a16146f Fix data objects being considered equal in DataClassEq (#6144) (#6145)
    • 47fe8d4 remove KotestInternal in TestFactory (#6146)
    • b39700c fix docs (#6147)
    • 8006dbd Remove usages of K1's KtUltraLightClass from kotest plugin sources (#6143)
    • 07b21d7 Add support for deprecated Kotlin targets in Android and Native conve… (#6141)
    • 96f71e0 docs: add KDoc to ArrayEq (#6140)
    • 7f2b14a Cut docs for 6.2
    • ead01c0 Override additional Kotlin version properties for IC-261 in release workflow
    • f3134ea align compile toolchain with minimum supported Kotlin version for KLIB ABI co...
    • Additional commits viewable in compare view

    Updates `io.kotest:kotest-assertions-json` from 6.1.11 to 6.2.1
    Release notes

    Sourced from io.kotest:kotest-assertions-json's releases.

    v6.2.1

    What's Changed

    Full Changelog: https://github.com/kotest/kotest/compare/v6.2.0...v6.2.1

    v6.2.0

    ✨ Features

    New matchers / assertions

    comparables.shouldBeAtLeast — @​AlexCue987 (#5831) MultipleMatcherResult for combining multiple matcher results — @​sksamuel (#5723) shouldContainExactCopies for lists — @​AlexCue987 (#5913) Character matchers — @​JordanLongstaff (#5921) shouldBeSingle matcher — @​sksamuel (#6028) shouldContainRepeats for strings — @​AlexCue987 (#6004) Infix form for inspectors — @​alfonsoristorato (#5920) Block-asserting overload for shouldBeFailure — @​alfonsoristorato (#5927) Per-call Eq overrides via a withEqs DSL — @​PreAgile (#6010) Per-element data class diffs in collection comparisons — @​PreAgile (#5835)

    Framework / API

    Run data tests singularly — @​alfonsoristorato (#5574) New JVM test-suites module (deprecates JunitXmlReporter) — @​sksamuel (#5801) Support isolation modes on all KMP platforms — @​sksamuel (#5806) Breadcrumbs support for Kotest spec files — @​sksamuel (#5793) Type-safe test metadata API — @​PreAgile (#5905) Public API for creating custom styles — @​sksamuel (#5814) Support multiple --test args joined by semicolon in KOTEST_INCLUDE_PATTERN — @​sksamuel (#5922) Extend life of deprecated Test containers and add onStart — @​sksamuel (#5896) Make Arb.stringPattern multiplatform — @​wilmveel (#6008) Complete the permutations module — @​sksamuel (#6016) Add Wasm WASI target to kotest-property and kotest-property-permutations — @​jsoizo (#6129) Make fibonacci interval functions public — @​mvanhorn (#6135) Add Android instrumented test module — @​sksamuel (#5900)

    IntelliJ plugin

    N-times setter for test invocation — @​alfonsoristorato (#5815) Invocation count on the Kotest producer — @​alfonsoristorato (#5837) Singular data-test run on the Kotest producer — @​alfonsoristorato (#5843) Amper-aware run-configuration producer — @​sksamuel (#5999)

    ... (truncated)

    Commits
    • 9ae8a1d Make flaky "throws FileNotFoundException for some time" eventually test deter...
    • a16146f Fix data objects being considered equal in DataClassEq (#6144) (#6145)
    • 47fe8d4 remove KotestInternal in TestFactory (#6146)
    • b39700c fix docs (#6147)
    • 8006dbd Remove usages of K1's KtUltraLightClass from kotest plugin sources (#6143)
    • 07b21d7 Add support for deprecated Kotlin targets in Android and Native conve… (#6141)
    • 96f71e0 docs: add KDoc to ArrayEq (#6140)
    • 7f2b14a Cut docs for 6.2
    • ead01c0 Override additional Kotlin version properties for IC-261 in release workflow
    • f3134ea align compile toolchain with minimum supported Kotlin version for KLIB ABI co...
    • Additional commits viewable in compare view

    Updates `io.kotest:kotest-assertions-ktor` from 6.1.11 to 6.2.1
    Release notes

    Sourced from io.kotest:kotest-assertions-ktor's releases.

    v6.2.1

    What's Changed

    Full Changelog: https://github.com/kotest/kotest/compare/v6.2.0...v6.2.1

    v6.2.0

    ✨ Features

    New matchers / assertions

    comparables.shouldBeAtLeast — @​AlexCue987 (#5831) MultipleMatcherResult for combining multiple matcher results — @​sksamuel (#5723) shouldContainExactCopies for lists — @​AlexCue987 (#5913) Character matchers — @​JordanLongstaff (#5921) shouldBeSingle matcher — @​sksamuel (#6028) shouldContainRepeats for strings — @​AlexCue987 (#6004) Infix form for inspectors — @​alfonsoristorato (#5920) Block-asserting overload for shouldBeFailure — @​alfonsoristorato (#5927) Per-call Eq overrides via a withEqs DSL — @​PreAgile (#6010) Per-element data class diffs in collection comparisons — @​PreAgile (#5835)

    Framework / API

    Run data tests singularly — @​alfonsoristorato (#5574) New JVM test-suites module (deprecates JunitXmlReporter) — @​sksamuel (#5801) Support isolation modes on all KMP platforms — @​sksamuel (#5806) Breadcrumbs support for Kotest spec files — @​sksamuel (#5793) Type-safe test metadata API — @​PreAgile (#5905) Public API for creating custom styles — @​sksamuel (#5814) Support multiple --test args joined by semicolon in KOTEST_INCLUDE_PATTERN — @​sksamuel (#5922) Extend life of deprecated Test containers and add onStart — @​sksamuel (#5896) Make Arb.stringPattern multiplatform — @​wilmveel (#6008) Complete the permutations module — @​sksamuel (#6016) Add Wasm WASI target to kotest-property and kotest-property-permutations — @​jsoizo (#6129) Make fibonacci interval functions public — @​mvanhorn (#6135) Add Android instrumented test module — @​sksamuel (#5900)

    IntelliJ plugin

    N-times setter for test invocation — @​alfonsoristorato (#5815) Invocation count on the Kotest producer — @​alfonsoristorato (#5837) Singular data-test run on the Kotest producer — @​alfonsoristorato (#5843) Amper-aware run-configuration producer — @​sksamuel (#5999)

    ... (truncated)

    Commits
    • 9ae8a1d Make flaky "throws FileNotFoundException for some time" eventually test deter...
    • a16146f Fix data objects being considered equal in DataClassEq (#6144) (#6145)
    • 47fe8d4 remove KotestInternal in TestFactory (#6146)
    • b39700c fix docs (#6147)
    • 8006dbd Remove usages of K1's KtUltraLightClass from kotest plugin sources (#6143)
    • 07b21d7 Add support for deprecated Kotlin targets in Android and Native conve… (#6141)
    • 96f71e0 docs: add KDoc to ArrayEq (#6140)
    • 7f2b14a Cut docs for 6.2
    • ead01c0 Override additional Kotlin version properties for IC-261 in release workflow
    • f3134ea align compile toolchain with minimum supported Kotlin version for KLIB ABI co...
    • Additional commits viewable in compare view

    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 0b2929d13..b35612ec0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ netty = "4.2.15.Final" awaitility = "4.3.0" collections-immutable = "0.4.0" coroutines = "1.11.0" -kotest = "6.1.11" +kotest = "6.2.1" kotlinx-io = "0.9.0" ktor = "3.4.3" logging = "8.0.4" From b6f4ad5a9fe52ef4b3937e224854e325771a4207 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:12:13 +0200 Subject: [PATCH 179/187] chore(deps): bump org.jetbrains.kotlinx:kotlinx-collections-immutable from 0.4.0 to 0.5.0 in the kotlinx group across 1 directory (#833) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the kotlinx group with 1 update in the / directory: [org.jetbrains.kotlinx:kotlinx-collections-immutable](https://github.com/Kotlin/kotlinx.collections.immutable). Updates `org.jetbrains.kotlinx:kotlinx-collections-immutable` from 0.4.0 to 0.5.0
    Release notes

    Sourced from org.jetbrains.kotlinx:kotlinx-collections-immutable's releases.

    v0.5.0

    This release renames the copy-returning methods on PersistentCollection, PersistentList, and PersistentMap to participial forms (KEEP-0459) — add→adding, put→putting, set→replacingAt, clear→cleared, and the rest. The old imperative names continue to compile in 0.5.x as @Deprecated(WARNING), become a compile error in 0.6.0, and are removed in 0.7.0. First shipped in 0.5.0-beta01; 0.5.0 is the stable release, with only internal fixes since.

    To migrate, apply the IDE quick-fixes, follow the migration guide, or use the 0.5.x migration skill with an AI assistant — each applies the rename the deprecation warnings name.

    Changes since 0.4.0:

    • Renamed copy-returning methods to participial forms; deprecated the old names #233
    • Enabled the Kotlin return-value checker (-Xreturn-value-checker=full) for the core module #243
    • Updated Kotlin to 2.3.0 #242
    • Configured JDK release 8 for the Kotlin compiler #239
    • Populated Implementation-* attributes in published JAR manifests #237
    • Enabled Dokka documentation generation, upgraded to Dokka 2.2.0 #225, #245, #248

    Full changelog: https://github.com/Kotlin/kotlinx.collections.immutable/compare/v0.4.0...v0.5.0

    v0.5.0-beta01

    This release renames copy-returning methods on PersistentCollection, PersistentList, and PersistentMap to participial forms — the old imperative names continue to compile in 0.5.x as @Deprecated(WARNING), become a compile error in 0.6.0, and are removed in 0.7.0. Concretely, add → adding, put → putting, clear → cleared, set → replacingAt, removeAt → removingAt, and the rest of the family (see the migration guide for the full table). The rename follows KEEP-0459 — Naming Conventions for Copy-Returning Operations.

    If your code uses any of these methods, please read the migration guide for the full rename table, the deprecation timeline, and the recommended IDE quick-fix workflow.

    This is a beta — 0.5.0 is planned to drop the -beta suffix soon, and once it ships stable the participial names are committed (any further rename would need its own deprecation cycle). If any of the renames feels awkward or you've hit a migration problem, please share it on [issue #232](Kotlin/kotlinx.collections.immutable#232) before then.

    What's changed

    • Renamed PersistentCollection mutating-copy methods to participial forms (add/remove/set/put/clear → adding/removing/setting/putting/clearing and *ed variants) and deprecated the original names #233
    • Enabled the Kotlin return-value checker (-Xreturn-value-checker=full) for the core module and annotated select internal helpers with @IgnorableReturnValue where discarding the result is intentional #243
    • Updated Kotlin to version 2.3.0 #242
    • Configured JDK release to 8 for the Kotlin compiler #239
    • Populated Implementation-Title/Implementation-Version/Implementation-Vendor in published JAR manifests #237
    • Enabled Dokka documentation generation and upgraded Dokka to 2.2.0 #225, #245, #248
    Changelog

    Sourced from org.jetbrains.kotlinx:kotlinx-collections-immutable's changelog.

    0.5.0

    • Promoted 0.5.0-beta01 to stable; no API or behavior changes

    0.5.0-beta01

    • Renamed copy-returning methods on PersistentCollection, PersistentList, and PersistentMap from imperative to participial forms (add/remove/put/clear/set/removeAt/… → adding/removing/putting/cleared/replacingAt/removingAt/…) per KEEP-0459 and deprecated the original names #233
    • Enabled the Kotlin return-value checker (-Xreturn-value-checker=full) for the core module and annotated select internal helpers with @IgnorableReturnValue where discarding the result is intentional #243
    • Updated Kotlin to version 2.3.0 #242
    • Configured JDK release to 8 for the Kotlin compiler #239
    • Populated Implementation-Title/Implementation-Version/Implementation-Vendor in published JAR manifests #237
    • Enabled Dokka documentation generation and upgraded Dokka to 2.2.0 #225, #245, #248
    Commits
    • 0727896 Release v0.5.0
    • 0e1a24c Document return values in the KDoc summary instead of @return (#256)
    • 2069a62 Fix equals/hashCode contract violation in random-operations stress tests (#262)
    • 6f7abb9 Updated team infra plugin (#260)
    • 15a77d7 Fix the package-list location (#258)
    • 1ab038c Add .kotlin to .gitignore to exclude build metadata produced by KGP (#259)
    • dc6ba40 Add top-level README for kotlinx-collections-immutable module (#255)
    • a6ad2e1 Release v0.5.0-beta01
    • 54f4815 Rename PersistentCollection methods to participial forms and deprecate origin...
    • 753924c Added missing reference to deployment id parameter (#250)
    • Additional commits viewable in compare view

    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 b35612ec0..cf6d3747e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ netty = "4.2.15.Final" # libraries version awaitility = "4.3.0" -collections-immutable = "0.4.0" +collections-immutable = "0.5.0" coroutines = "1.11.0" kotest = "6.2.1" kotlinx-io = "0.9.0" From d93f47f1e57a2c1fbdd835679bae7e701cc15096 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:14:48 +0200 Subject: [PATCH 180/187] chore(deps): bump ch.qos.logback:logback-classic from 1.5.34 to 1.5.37 (#857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.34 to 1.5.37.
    Release notes

    Sourced from ch.qos.logback:logback-classic's releases.

    Logback 1.5.37

    2026-06-26 Release of logback version 1.5.37

    • Given the numerous vulnerabilities related to conditional configuration processing based on the evaluation of Java expressions using the Janino library, support for such expressions has been removed. Users are offered the an online migration service or the element introduced in version 1.5.20. See the relevant documentation for more details.

    • A bitwise identical binary of this version can be reproduced by building from source code at commit c1df7f522e648eec7b4ef6a12c8758fec0f00048 associated with the tag v_1.5.37. Release built using Java "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian 11.6.

    Logback 1.5.36

    2026-06-25 Release of logback version 1.5.36

    • The 'condition' attribute in <if> elements now reject certain references that are associated with ACE attacks. This issue was reported by "yulate" (yulate531@gmail.com.com) and registered as CVE-2026-13006.

    • A bitwise identical binary of this version can be reproduced by building from source code at commit 9b94c37562bf25a6a944146701d42ee6c4eee888 associated with the tag v_1.5.36. Release built using Java "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian 11.6.

    Logback 1.5.35

    026-06-23 Release of logback version 1.5.35

    • The 'condition' attribute in <if> elements now rejects unicode escape sequences (\u and \U). This closes a bypass of the existing prohibition on the new operator in Janino-evaluated conditions. This issue was reported by IcySun (icysun@qq.com) and registered as CVE-2026-13006.

    • Added ConfiguratorRank.AUTHENTICATING (rank 100), the highest configurator rank, for certified/authenticating configurators discovered via the ServiceLoader mechanism. ContextInitializer now requires that at most one such configurator exist on the classpath; if more than one is found, initialization aborts with an error.

    • ConsoleCharsetPropertyDefiner is no longer shipped. The Java 21 multi-release compilation of logback-core has been disabled, which removes this class from the published artifact. Configurations that referenced ch.qos.logback.core.property.ConsoleCharsetPropertyDefiner will need an alternative approach for console charset detection.

    • The logback-examples module is now included in artifacts published to Maven Central.

    • JoranConfigurator.makeAnotherInstance() and DefaultJoranConfigurator.performMultiStepConfigurationFileSearch() are now protected, allowing derived configurators to override these methods.

    • A bitwise identical binary of this version can be reproduced by building from source code at commit 08bd1598d565d83444f72983935e7da4746783b7 associated with the tag v_1.5.35. Release built using Java "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian 11.6.

    Commits

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ch.qos.logback:logback-classic&package-manager=gradle&previous-version=1.5.34&new-version=1.5.37)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    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 cf6d3747e..6faaf2ae4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ logging = "8.0.4" mockk = "1.14.11" serialization = "1.11.0" slf4j = "2.0.18" -logback = "1.5.34" +logback = "1.5.37" junit = "5.14.4" [libraries] From 4103b87d6952f7e72b0fc9ddfdb2f59a0a39d0ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:17:43 +0200 Subject: [PATCH 181/187] chore(deps): bump org.jetbrains.kotlinx:kotlinx-io-core from 0.9.0 to 0.9.1 in the kotlinx group across 1 directory (#855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the kotlinx group with 1 update in the / directory: [org.jetbrains.kotlinx:kotlinx-io-core](https://github.com/Kotlin/kotlinx-io). Updates `org.jetbrains.kotlinx:kotlinx-io-core` from 0.9.0 to 0.9.1
    Release notes

    Sourced from org.jetbrains.kotlinx:kotlinx-io-core's releases.

    v0.9.1

    Features

    • Dropped an old workaround for unsupported Android version #508
    • Reimplemented module loading for WasmJs #506

    Thanks to @​ilgonmic and @​MohammedKHC for their contributions!

    Changelog

    Sourced from org.jetbrains.kotlinx:kotlinx-io-core's changelog.

    0.9.1

    Published 26 June 2026

    Features

    • Dropped an old workaround for unsupported Android version #508
    • Reimplemented module loading for WasmJs #506
    Commits
    • 362bdc3 Release 0.9.1
    • e5ae2f0 Merge branch 'develop'
    • 2997caf Drop workaround for Android 4.2.2 getsockname bug (#508)
    • a1d6bd9 Use IIFE to asynchronously load a module instead of globalThis (#506)
    • d53d449 Update feedback channels in contributing guidelines
    • 34f1579 Merge branch 'develop'
    • 82caeb2 Document segment pool configuration (#494)
    • See full diff in compare view

    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 6faaf2ae4..ae38db600 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ awaitility = "4.3.0" collections-immutable = "0.5.0" coroutines = "1.11.0" kotest = "6.2.1" -kotlinx-io = "0.9.0" +kotlinx-io = "0.9.1" ktor = "3.4.3" logging = "8.0.4" mockk = "1.14.11" From cd91652be115064c0b9fc43ba1c517893131ba92 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Mon, 29 Jun 2026 14:38:44 +0200 Subject: [PATCH 182/187] fix: validate Origin against localhost by default in DNS rebinding protection (#840) Default the `Origin` allowlist to localhost when `DnsRebindingProtection` relies on the localhost host defaults, so a request with a valid `Host` but a hostile (or `null`) `Origin` is no longer accepted. ## How Has This Been Tested? unit tests ## Breaking Changes Behavioral change only ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .../kotlin/sdk/server/HostValidation.kt | 10 ++++ .../kotlin/sdk/server/KtorServer.kt | 21 +++++++-- .../sdk/server/DnsRebindingProtectionTest.kt | 46 +++++++++++++++++++ 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt index 88b56bc17..ef010fe60 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/HostValidation.kt @@ -19,6 +19,16 @@ import io.modelcontextprotocol.kotlin.sdk.types.RPCError */ internal val LOCALHOST_ALLOWED_HOSTS: List = listOf("localhost", "127.0.0.1", "[::1]") +/** + * Default list of `Origin` values allowed for localhost DNS rebinding protection. + * + * Mirrors [LOCALHOST_ALLOWED_HOSTS] but carries a scheme so the values parse as URLs: + * [extractOriginHost] rejects schemeless input. Comparison is hostname-only and + * scheme-agnostic, so these entries also match `https://` origins on the same host. + */ +internal val LOCALHOST_ALLOWED_ORIGINS: List = + listOf("http://localhost", "http://127.0.0.1", "http://[::1]") + /** * Characters that are valid in a URL but must not appear in an HTTP `Host` header. * Rejecting them prevents the parser from accepting malformed values 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 d6191dffb..7e47d7652 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 @@ -41,7 +41,9 @@ private val logger = KotlinLogging.logger {} * @param allowedHosts hostnames allowed in the `Host` header. Defaults to `localhost`, `127.0.0.1`, `[::1]`. * @param allowedOrigins origins allowed in the `Origin` header, compared by hostname only * (scheme and port are ignored). Requests without an `Origin` header are allowed. - * Pass `null` to skip origin validation. + * When `null` while the localhost host defaults are in effect (no custom `allowedHosts`), + * the `Origin` header is validated against `localhost`, `127.0.0.1`, `[::1]`. + * With custom `allowedHosts`, `null` skips origin validation. * @param block factory block with access to the [ServerSSESession] * that creates and returns the [Server] to handle the connection. * @throws IllegalStateException if the [SSE] plugin is not installed. @@ -71,7 +73,9 @@ public fun Route.mcp( * @param allowedHosts hostnames allowed in the `Host` header. Defaults to `localhost`, `127.0.0.1`, `[::1]`. * @param allowedOrigins origins allowed in the `Origin` header, compared by hostname only * (scheme and port are ignored). Requests without an `Origin` header are allowed. - * Pass `null` to skip origin validation. + * When `null` while the localhost host defaults are in effect (no custom `allowedHosts`), + * the `Origin` header is validated against `localhost`, `127.0.0.1`, `[::1]`. + * With custom `allowedHosts`, `null` skips origin validation. * @param block factory block with access to the [ServerSSESession] * that creates and returns the [Server] to handle the connection. * @throws IllegalStateException if the [SSE] plugin is not installed. @@ -119,7 +123,9 @@ public fun Route.mcp( * @param allowedHosts hostnames allowed in the `Host` header. Defaults to `localhost`, `127.0.0.1`, `[::1]`. * @param allowedOrigins origins allowed in the `Origin` header, compared by hostname only * (scheme and port are ignored). Requests without an `Origin` header are allowed. - * Pass `null` to skip origin validation. + * When `null` while the localhost host defaults are in effect (no custom `allowedHosts`), + * the `Origin` header is validated against `localhost`, `127.0.0.1`, `[::1]`. + * With custom `allowedHosts`, `null` skips origin validation. * @param block factory block with access to the [ServerSSESession] * that creates and returns the [Server] to handle the connection. */ @@ -205,7 +211,9 @@ private fun Application.mcpStreamableHttp( * If `null` and DNS rebinding protection is enabled, defaults to `localhost`, `127.0.0.1`, `[::1]`. * @param allowedOrigins A list of allowed `Origin` header values, compared by hostname only * (scheme and port are ignored). Requests without an `Origin` header are allowed. - * If `null`, origin validation is disabled. + * When `null` while the localhost host defaults are in effect (no custom `allowedHosts`), + * the `Origin` header is validated against `localhost`, `127.0.0.1`, `[::1]`. + * With custom `allowedHosts`, `null` skips origin validation. * @param eventStore An optional [EventStore] instance to enable resumable event stream functionality. * Allows storing and replaying events. * @param block factory block with access to the [RoutingContext] (for reading request headers) @@ -451,6 +459,9 @@ private fun Route.installDnsRebindingProtection(enabled: Boolean, hosts: List Date: Mon, 29 Jun 2026 15:07:06 +0200 Subject: [PATCH 183/187] chore(deps): bump gradle-wrapper from 9.6.0 to 9.6.1 (#856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [gradle-wrapper](https://github.com/gradle/gradle) from 9.6.0 to 9.6.1.
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.1

    The Gradle team is excited to announce Gradle 9.6.1.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.1 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.1 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • 309d128 Update fixed issues in release notes for 9.6.1 (#38328)
    • 040a978 Update fixed issues in release notes for 9.6.1
    • e0b8325 Restore --non-interactive flag instead of --interactive/--no-interactive (#38...
    • 946f3e6 Limit explicit temp file permission setting to intended use case (#38300)
    • 65f8224 Restore --non-interactive flag instead of --interactive/--no-interactive
    • e346a5e Adjust CLI flag to configure non-interactive console (#38301)
    • 9b53be9 Adjust CLI flag to configure non-interactive console
    • 0dd3b53 Limit explicit temp file permission setting to intended use case
    • 48e5ac2 Add reproducers
    • 25598fd Prepare 9.6.1 patch release (#38293)
    • Additional commits viewable in compare view

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index eb84db68d..a9db11550 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 From 19db4b04e3d578635d4a60e190bc5f9f23bc53f4 Mon Sep 17 00:00:00 2001 From: Takuya Akamatsu Date: Tue, 30 Jun 2026 00:18:53 +0900 Subject: [PATCH 184/187] fix(client): gate completion/complete on the completions capability (#846) Gate `completion/complete` on the server's `completions` capability instead of `prompts`. closes #845 ## Motivation and Context A server that declares `completions` but not `prompts` is currently rejected for completion. Details in #845. ## How Has This Been Tested? New `ClientAssertCapabilityTest` cases; they fail on `main` and pass with this change. `apiCheck` / `ktlintCheck` / `detekt` pass. ## Breaking Changes None. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed Co-authored-by: Pavel Gorgulov --- .../kotlin/sdk/client/Client.kt | 9 +++++++-- .../sdk/client/ClientAssertCapabilityTest.kt | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index b88cb1b68..59f2b7e4b 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -242,13 +242,18 @@ public open class Client(private val clientInfo: Implementation, options: Client Method.Defined.PromptsGet, Method.Defined.PromptsList, - Method.Defined.CompletionComplete, -> { checkNotNull(serverCapabilities?.prompts) { "Server does not support prompts (required for $method)" } } + Method.Defined.CompletionComplete -> { + checkNotNull(serverCapabilities?.completions) { + "Server does not support completions (required for $method)" + } + } + Method.Defined.ResourcesList, Method.Defined.ResourcesTemplatesList, Method.Defined.ResourcesRead, @@ -394,7 +399,7 @@ public open class Client(private val clientInfo: Implementation, options: Client * @param params The completion request parameters. * @param options Optional request options. * @return The completion result returned by the server, or `null` if none. - * @throws IllegalStateException If the server does not support prompts or completion. + * @throws IllegalStateException If the server does not support completions. */ public suspend fun complete(params: CompleteRequest, options: RequestOptions? = null): CompleteResult = request(params, options) diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt index 2f322ec90..e9379a7f0 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt @@ -84,6 +84,25 @@ class ClientAssertCapabilityTest { ex.message.orEmpty() shouldContain "Client does not support tasks" } + @Test + fun `CompletionComplete does not throw when server declared completions`() = runTest { + val client = newTestClient( + serverCapabilities = ServerCapabilities(completions = ServerCapabilities.Completions), + ) + client.exposedAssertCapabilityForMethod(Method.Defined.CompletionComplete) + } + + @Test + fun `CompletionComplete throws when server has no completions capability`() = runTest { + val client = newTestClient( + serverCapabilities = ServerCapabilities(prompts = ServerCapabilities.Prompts()), + ) + val ex = assertFailsWith { + client.exposedAssertCapabilityForMethod(Method.Defined.CompletionComplete) + } + ex.message.orEmpty() shouldContain "Server does not support completions" + } + private suspend fun newTestClient( serverCapabilities: ServerCapabilities = ServerCapabilities(), clientCapabilities: ClientCapabilities = ClientCapabilities(), From 0221b8db3d596a8add753786048126564ad70b78 Mon Sep 17 00:00:00 2001 From: King Star Date: Tue, 30 Jun 2026 15:46:43 +0800 Subject: [PATCH 185/187] fix(client): close stdio read sources during shutdown (#798) ## Summary Fixes `StdioClientTransport.close()` so shutdown does not hang when the stdin read loop is blocked waiting for input. ## Root Cause / Context `StdioClientTransport.closeResources()` stopped the transport coroutine scope and then waited for the scope job to finish. On the JVM, a `Source.readAtMostTo()` call backed by a blocking Java stream does not necessarily return when the coroutine is cancelled. If the read loop stayed blocked, `close()` could wait indefinitely. ## Changes - Close the stdin source during stdio client shutdown before waiting for transport coroutines to finish. - Also close the optional stderr source during shutdown. - Keep source-close failures non-fatal and log them at debug level. - Add a JVM regression test with a `RawSource` that blocks in `readAtMostTo()` until `close()` is called. - Assert the close path completes, emits `onClose`, and does not invoke `onError`. ## Scope / Risk This is limited to `StdioClientTransport` shutdown behavior. It does not change the public API or server transports. The regression coverage is JVM-focused because the reported hang is caused by a blocking Java stream read. ## Verification ```bash ./gradlew :integration-test:jvmTest --tests 'io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransportTest' ./gradlew :kotlin-sdk-client:jvmTest ./gradlew :kotlin-sdk-client:ktlintCheck :integration-test:ktlintCheck :kotlin-sdk-client:detekt :integration-test:detekt :kotlin-sdk-client:apiCheck ``` Closes #514 --- .../sdk/client/StdioClientTransportTest.kt | 61 +++++++++++++++++++ .../kotlin/sdk/client/StdioClientTransport.kt | 10 +++ 2 files changed, 71 insertions(+) diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt index 585af3f8d..fc3c1d3dd 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt @@ -7,9 +7,13 @@ import io.modelcontextprotocol.kotlin.sdk.types.McpException import io.modelcontextprotocol.kotlin.test.utils.createSleepyProcessBuilder import io.modelcontextprotocol.kotlin.test.utils.createTeeProcessBuilder import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.io.Buffer +import kotlinx.io.RawSource import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered @@ -18,10 +22,12 @@ import org.junit.jupiter.api.Timeout import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.condition.DisabledOnOs import org.junit.jupiter.api.condition.OS +import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.test.fail import kotlin.time.Duration.Companion.seconds @@ -109,6 +115,43 @@ class StdioClientTransportTest : BaseTransportTest() { } } + @OptIn(ExperimentalAtomicApi::class) + @Test + fun `should close cleanly while stdin read is blocked`(): Unit = runBlocking(Dispatchers.IO) { + val input = BlockingRawSource() + + val transport = StdioClientTransport( + input = input.buffered(), + output = Buffer(), + ) + + val didClose = AtomicBoolean(false) + transport.onClose { didClose.store(true) } + transport.onError { error -> + fail("Unexpected error while closing transport: $error") + } + + transport.start() + + eventually(2.seconds) { + assertTrue(input.readStarted, "Transport should start reading stdin before close") + } + + val closeJob = async { + transport.close() + } + + val closed = withTimeoutOrNull(1.seconds) { + closeJob.await() + } + + input.close() + closeJob.await() + + assertNotNull(closed, "Transport.close() should not wait for stdin to produce data or EOF") + assertTrue(didClose.load(), "Transport should be closed after close() call") + } + @Test fun `should read messages`() = runTest { val processBuilder = createTeeProcessBuilder() @@ -147,4 +190,22 @@ class StdioClientTransportTest : BaseTransportTest() { process.waitFor() process.destroyForcibly() } + + private class BlockingRawSource : RawSource { + private val closed = CountDownLatch(1) + + @Volatile + var readStarted: Boolean = false + private set + + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long { + readStarted = true + closed.await() + return -1L + } + + override fun close() { + closed.countDown() + } + } } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt index 63f681c5c..5e990eea0 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt @@ -244,10 +244,20 @@ public class StdioClientTransport @JvmOverloads public constructor( override suspend fun closeResources() { withContext(NonCancellable) { scope.stopProcessing("Closed") + closeReadSources() scope.coroutineContext[Job]?.join() // Wait for all coroutines to complete } } + private fun closeReadSources() { + runCatching { input.close() } + .onFailure { logger.debug(it) { "Error closing stdin source" } } + error?.let { source -> + runCatching { source.close() } + .onFailure { logger.debug(it) { "Error closing stderr source" } } + } + } + private fun sendOutboundMessage(message: JSONRPCMessage, sink: Sink, mainScope: CoroutineScope) { try { val json = serializeMessage(message) From d2b1509147722305da481e3c939940253b066ecd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:44:40 +0200 Subject: [PATCH 186/187] chore(deps): bump the other-dependencies group across 5 directories with 5 updates (#865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the other-dependencies group with 3 updates in the /samples/kotlin-mcp-client directory: [gradle-wrapper](https://github.com/gradle/gradle), [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java) and [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 2 updates in the /samples/kotlin-mcp-server directory: [gradle-wrapper](https://github.com/gradle/gradle) and [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 3 updates in the /samples/kotlinlang-mcp-server directory: [gradle-wrapper](https://github.com/gradle/gradle), [com.gradleup.shadow](https://github.com/GradleUp/shadow) and [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback). Bumps the other-dependencies group with 3 updates in the /samples/simple-streamable-server directory: [gradle-wrapper](https://github.com/gradle/gradle), [com.gradleup.shadow](https://github.com/GradleUp/shadow) and [org.junit.jupiter:junit-jupiter](https://github.com/junit-team/junit-framework). Bumps the other-dependencies group with 2 updates in the /samples/weather-stdio-server directory: [gradle-wrapper](https://github.com/gradle/gradle) and [com.gradleup.shadow](https://github.com/GradleUp/shadow). Updates `gradle-wrapper` from 9.6.0 to 9.6.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.1

    The Gradle team is excited to announce Gradle 9.6.1.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.1 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.1 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • 309d128 Update fixed issues in release notes for 9.6.1 (#38328)
    • 040a978 Update fixed issues in release notes for 9.6.1
    • e0b8325 Restore --non-interactive flag instead of --interactive/--no-interactive (#38...
    • 946f3e6 Limit explicit temp file permission setting to intended use case (#38300)
    • 65f8224 Restore --non-interactive flag instead of --interactive/--no-interactive
    • e346a5e Adjust CLI flag to configure non-interactive console (#38301)
    • 9b53be9 Adjust CLI flag to configure non-interactive console
    • 0dd3b53 Limit explicit temp file permission setting to intended use case
    • 48e5ac2 Add reproducers
    • 25598fd Prepare 9.6.1 patch release (#38293)
    • Additional commits viewable in compare view

    Updates `com.anthropic:anthropic-java` from 2.43.0 to 2.45.0
    Release notes

    Sourced from com.anthropic:anthropic-java's releases.

    v2.45.0

    2.45.0 (2026-06-29)

    Full Changelog: v2.44.1...v2.45.0

    Features

    • api: add support for 20260318 web fetch and support tools (f38d0e6)

    Bug Fixes

    • add web tools 20260318 delegate overloads to StructuredMessageCreateParams (#120) (a66f7a3)

    v2.44.1

    2.44.1 (2026-06-26)

    Full Changelog: v2.44.0...v2.44.1

    Bug Fixes

    • bedrock: use daemon threads for the SSE transcoding pool (#97) (0678c4d)
    • skill creation (#119) (5dadc03)

    Chores

    • api: accept user profile ID's when counting tokens (ee8b72d)
    • delete unnecessary dir (#108) (35a9662)
    • docs: updates to descriptions and example values (6686e6b)

    Build System

    • add detekt rules (#109) (86b8f8d)
    • derive umbrella Dokka aggregation from re-exported modules (#110) (82c00c7)

    v2.44.0

    2.44.0 (2026-06-24)

    Full Changelog: v2.43.0...v2.44.0

    Features

    • client: add support for system.message streaming events (e27e961)

    Chores

    • api: add support for new refusal category (8c1bd29)

    ... (truncated)

    Changelog

    Sourced from com.anthropic:anthropic-java's changelog.

    2.45.0 (2026-06-29)

    Full Changelog: v2.44.1...v2.45.0

    Features

    • api: add support for 20260318 web fetch and support tools (f38d0e6)

    Bug Fixes

    • add web tools 20260318 delegate overloads to StructuredMessageCreateParams (#120) (a66f7a3)

    2.44.1 (2026-06-26)

    Full Changelog: v2.44.0...v2.44.1

    Bug Fixes

    • bedrock: use daemon threads for the SSE transcoding pool (#97) (0678c4d)
    • skill creation (#119) (5dadc03)

    Chores

    • api: accept user profile ID's when counting tokens (ee8b72d)
    • delete unnecessary dir (#108) (35a9662)
    • docs: updates to descriptions and example values (6686e6b)

    Build System

    • add detekt rules (#109) (86b8f8d)
    • derive umbrella Dokka aggregation from re-exported modules (#110) (82c00c7)

    2.44.0 (2026-06-24)

    Full Changelog: v2.43.0...v2.44.0

    Features

    • client: add support for system.message streaming events (e27e961)

    Chores

    • api: add support for new refusal category (8c1bd29)
    • api: add support for sending User Profile ID in request headers (8d69cbe)

    ... (truncated)

    Commits
    • 66da1ba release: 2.45.0
    • ae4a759 fix: add web tools 20260318 delegate overloads to StructuredMessageCreatePara...
    • 89455ee feat(api): add support for 20260318 web fetch and support tools
    • ca57c4e release: 2.44.1
    • 2e9d127 fix: skill creation (#119)
    • 4ee08d6 fix(bedrock): use daemon threads for the SSE transcoding pool (#97)
    • b6bf663 chore(docs): updates to descriptions and example values
    • fea6ca0 build: add detekt rules (#109)
    • 35963af chore(api): accept user profile ID's when counting tokens
    • 0b00c21 chore: delete unnecessary dir (#108)
    • Additional commits viewable in compare view

    Updates `com.gradleup.shadow` from 9.4.2 to 9.4.3
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.3

    Changed

    • Update dependencies for resolving CVEs. (#2069)
    Commits

    Updates `gradle-wrapper` from 9.6.0 to 9.6.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.1

    The Gradle team is excited to announce Gradle 9.6.1.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.1 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.1 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • 309d128 Update fixed issues in release notes for 9.6.1 (#38328)
    • 040a978 Update fixed issues in release notes for 9.6.1
    • e0b8325 Restore --non-interactive flag instead of --interactive/--no-interactive (#38...
    • 946f3e6 Limit explicit temp file permission setting to intended use case (#38300)
    • 65f8224 Restore --non-interactive flag instead of --interactive/--no-interactive
    • e346a5e Adjust CLI flag to configure non-interactive console (#38301)
    • 9b53be9 Adjust CLI flag to configure non-interactive console
    • 0dd3b53 Limit explicit temp file permission setting to intended use case
    • 48e5ac2 Add reproducers
    • 25598fd Prepare 9.6.1 patch release (#38293)
    • Additional commits viewable in compare view

    Updates `com.gradleup.shadow` from 9.4.2 to 9.4.3
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.3

    Changed

    • Update dependencies for resolving CVEs. (#2069)
    Commits

    Updates `gradle-wrapper` from 9.6.0 to 9.6.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.1

    The Gradle team is excited to announce Gradle 9.6.1.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.1 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.1 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • 309d128 Update fixed issues in release notes for 9.6.1 (#38328)
    • 040a978 Update fixed issues in release notes for 9.6.1
    • e0b8325 Restore --non-interactive flag instead of --interactive/--no-interactive (#38...
    • 946f3e6 Limit explicit temp file permission setting to intended use case (#38300)
    • 65f8224 Restore --non-interactive flag instead of --interactive/--no-interactive
    • e346a5e Adjust CLI flag to configure non-interactive console (#38301)
    • 9b53be9 Adjust CLI flag to configure non-interactive console
    • 0dd3b53 Limit explicit temp file permission setting to intended use case
    • 48e5ac2 Add reproducers
    • 25598fd Prepare 9.6.1 patch release (#38293)
    • Additional commits viewable in compare view

    Updates `com.gradleup.shadow` from 9.4.2 to 9.4.3
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.3

    Changed

    • Update dependencies for resolving CVEs. (#2069)
    Commits

    Updates `gradle-wrapper` from 9.6.0 to 9.6.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.1

    The Gradle team is excited to announce Gradle 9.6.1.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.1 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.1 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • 309d128 Update fixed issues in release notes for 9.6.1 (#38328)
    • 040a978 Update fixed issues in release notes for 9.6.1
    • e0b8325 Restore --non-interactive flag instead of --interactive/--no-interactive (#38...
    • 946f3e6 Limit explicit temp file permission setting to intended use case (#38300)
    • 65f8224 Restore --non-interactive flag instead of --interactive/--no-interactive
    • e346a5e Adjust CLI flag to configure non-interactive console (#38301)
    • 9b53be9 Adjust CLI flag to configure non-interactive console
    • 0dd3b53 Limit explicit temp file permission setting to intended use case
    • 48e5ac2 Add reproducers
    • 25598fd Prepare 9.6.1 patch release (#38293)
    • Additional commits viewable in compare view

    Updates `com.gradleup.shadow` from 9.4.2 to 9.4.3
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.3

    Changed

    • Update dependencies for resolving CVEs. (#2069)
    Commits

    Updates `gradle-wrapper` from 9.6.0 to 9.6.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.1

    The Gradle team is excited to announce Gradle 9.6.1.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.1 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.1 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • 309d128 Update fixed issues in release notes for 9.6.1 (#38328)
    • 040a978 Update fixed issues in release notes for 9.6.1
    • e0b8325 Restore --non-interactive flag instead of --interactive/--no-interactive (#38...
    • 946f3e6 Limit explicit temp file permission setting to intended use case (#38300)
    • 65f8224 Restore --non-interactive flag instead of --interactive/--no-interactive
    • e346a5e Adjust CLI flag to configure non-interactive console (#38301)
    • 9b53be9 Adjust CLI flag to configure non-interactive console
    • 0dd3b53 Limit explicit temp file permission setting to intended use case
    • 48e5ac2 Add reproducers
    • 25598fd Prepare 9.6.1 patch release (#38293)
    • Additional commits viewable in compare view

    Updates `com.gradleup.shadow` from 9.4.2 to 9.4.3
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.3

    Changed

    • Update dependencies for resolving CVEs. (#2069)
    Commits

    Updates `gradle-wrapper` from 9.6.0 to 9.6.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.1

    The Gradle team is excited to announce Gradle 9.6.1.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.1 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.1 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • 309d128 Update fixed issues in release notes for 9.6.1 (#38328)
    • 040a978 Update fixed issues in release notes for 9.6.1
    • e0b8325 Restore --non-interactive flag instead of --interactive/--no-interactive (#38...
    • 946f3e6 Limit explicit temp file permission setting to intended use case (#38300)
    • 65f8224 Restore --non-interactive flag instead of --interactive/--no-interactive
    • e346a5e Adjust CLI flag to configure non-interactive console (#38301)
    • 9b53be9 Adjust CLI flag to configure non-interactive console
    • 0dd3b53 Limit explicit temp file permission setting to intended use case
    • 48e5ac2 Add reproducers
    • 25598fd Prepare 9.6.1 patch release (#38293)
    • Additional commits viewable in compare view

    Updates `com.gradleup.shadow` from 9.4.2 to 9.4.3
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.3

    Changed

    • Update dependencies for resolving CVEs. (#2069)
    Commits

    Updates `gradle-wrapper` from 9.6.0 to 9.6.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.1

    The Gradle team is excited to announce Gradle 9.6.1.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.1 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.1 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • 309d128 Update fixed issues in release notes for 9.6.1 (#38328)
    • 040a978 Update fixed issues in release notes for 9.6.1
    • e0b8325 Restore --non-interactive flag instead of --interactive/--no-interactive (#38...
    • 946f3e6 Limit explicit temp file permission setting to intended use case (#38300)
    • 65f8224 Restore --non-interactive flag instead of --interactive/--no-interactive
    • e346a5e Adjust CLI flag to configure non-interactive console (#38301)
    • 9b53be9 Adjust CLI flag to configure non-interactive console
    • 0dd3b53 Limit explicit temp file permission setting to intended use case
    • 48e5ac2 Add reproducers
    • 25598fd Prepare 9.6.1 patch release (#38293)
    • Additional commits viewable in compare view

    Updates `com.gradleup.shadow` from 9.4.2 to 9.4.3
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.3

    Changed

    • Update dependencies for resolving CVEs. (#2069)
    Commits

    Updates `gradle-wrapper` from 9.6.0 to 9.6.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.1

    The Gradle team is excited to announce Gradle 9.6.1.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.1 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.1 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes and other considerations when upgrading.

    For Java, Groovy, Kotlin and Android compatibility, see the full compatibility notes.

    Reporting problems

    If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure you're encountering a bug, please use the forum.

    We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.

    Commits
    • 309d128 Update fixed issues in release notes for 9.6.1 (#38328)
    • 040a978 Update fixed issues in release notes for 9.6.1
    • e0b8325 Restore --non-interactive flag instead of --interactive/--no-interactive (#38...
    • 946f3e6 Limit explicit temp file permission setting to intended use case (#38300)
    • 65f8224 Restore --non-interactive flag instead of --interactive/--no-interactive
    • e346a5e Adjust CLI flag to configure non-interactive console (#38301)
    • 9b53be9 Adjust CLI flag to configure non-interactive console
    • 0dd3b53 Limit explicit temp file permission setting to intended use case
    • 48e5ac2 Add reproducers
    • 25598fd Prepare 9.6.1 patch release (#38293)
    • Additional commits viewable in compare view

    Updates `com.gradleup.shadow` from 9.4.2 to 9.4.3
    Release notes

    Sourced from com.gradleup.shadow's releases.

    9.4.3

    Changed

    • Update dependencies for resolving CVEs. (#2069)
    Commits

    Updates `gradle-wrapper` from 9.6.0 to 9.6.1
    Release notes

    Sourced from gradle-wrapper's releases.

    9.6.1

    The Gradle team is excited to announce Gradle 9.6.1.

    Here are the highlights of this release:

    • Improved Configuration Cache hit rates
    • Additional CLI rendering options
    • Important project hierarchy lookup deprecations

    Read the Release Notes

    We would like to thank the following community members for their contributions to this release of Gradle: Aharnish Solanki, Benedikt Johannes, Devendra Reddy Pennabadi, Dmytro Rodionov, Dreeam, Elías Hernández Rodríguez, Eng Zer Jun, FinlayRJW, Kamal Kansal, Marcono1234, Nelson Osacky, Philip Wedemann, Ravi, Roberto Perez Alcolea, Ryan Schmitt, Sebastian Schuberth, seunghun.ham, sk-reddy17, Suvrat Acharya, Vedant Madane.

    Upgrade instructions

    Switch your build to use Gradle 9.6.1 by updating your wrapper:

    ./gradlew :wrapper --gradle-version=9.6.1 &&
    ./gradlew :wrapper
    

    See the Gradle 9.x upgrade guide to l... _Description has been truncated_ Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 4 ++-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/kotlinlang-mcp-server/gradle/libs.versions.toml | 4 ++-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/simple-streamable-server/gradle/libs.versions.toml | 4 ++-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index 747918697..0b9018dad 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -anthropic = "2.43.0" +anthropic = "2.45.0" kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.13.0" -shadow = "9.4.2" +shadow = "9.4.3" slf4j = "2.0.18" [libraries] diff --git a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties index eb84db68d..a9db11550 100644 --- a/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index bd87373d9..56bf5f1b4 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ kotlin = "2.3.21" ktor = "3.3.3" mcp-kotlin = "0.13.0" slf4j = "2.0.18" -shadow = "9.4.2" +shadow = "9.4.3" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties index eb84db68d..a9db11550 100644 --- a/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml index 69dfe7e9b..26e89b27b 100644 --- a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -4,8 +4,8 @@ ktor = "3.3.3" mcp-kotlin = "0.13.0" serialization = "1.11.0" coroutines = "1.11.0" -logback = "1.5.35" -shadow = "9.4.2" +logback = "1.5.37" +shadow = "9.4.3" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties index eb84db68d..a9db11550 100644 --- a/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/kotlinlang-mcp-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index ba5b0e7c0..723994b2a 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] kotlin = "2.3.21" ktor = "3.3.3" -junit = "6.1.0" +junit = "6.1.1" mcp-kotlin = "0.13.0" slf4j = "2.0.18" -shadow = "9.4.2" +shadow = "9.4.3" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties index eb84db68d..a9db11550 100644 --- a/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/simple-streamable-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index 3e99eadae..e8052362d 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -4,7 +4,7 @@ kotlin = "2.3.21" ktor = "3.3.3" logging = "8.0.4" mcp-kotlin = "0.13.0" -shadow = "9.4.2" +shadow = "9.4.3" slf4j = "2.0.18" [libraries] diff --git a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties index eb84db68d..a9db11550 100644 --- a/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties +++ b/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 From bbf77bae93970d382ef97bf1569d57ed6d434ded Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:18:27 +0200 Subject: [PATCH 187/187] chore(deps): bump @types/node from 26.0.0 to 26.0.1 in /integration-test/src/jvmTest/typescript in the all-dependencies group across 1 directory (#864) Bumps the all-dependencies group with 1 update in the /integration-test/src/jvmTest/typescript directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 26.0.0 to 26.0.1

    Commits

    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 8 ++++---- integration-test/src/jvmTest/typescript/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 0fa51a9b6..befd50e35 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -15,7 +15,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^26.0.0", + "@types/node": "^26.0.1", "cors": "^2.8.6", "tsx": "^4.22.4", "typescript": "^6.0.3" @@ -579,9 +579,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", - "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index 00f0961c5..aa8d9dbf4 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^26.0.0", + "@types/node": "^26.0.1", "cors": "^2.8.6", "tsx": "^4.22.4", "typescript": "^6.0.3"