diff --git a/build.gradle.kts b/build.gradle.kts
index dc72359..57dd810 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -26,7 +26,7 @@ subprojects {
detekt {
config = files("$rootDir/config/detekt/detekt.yml")
buildUponDefaultConfig = true
- failOnSeverity.set(FailOnSeverity.Never)
+ failOnSeverity.set(FailOnSeverity.Error)
}
}
diff --git a/conformance-test/detekt-baseline.xml b/conformance-test/detekt-baseline.xml
new file mode 100644
index 0000000..fe120f5
--- /dev/null
+++ b/conformance-test/detekt-baseline.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ BracesOnWhenStatements:ConformanceServer.kt:HttpServerTransport$when
+ BracesOnWhenStatements:ConformanceTest.kt:ConformanceTest$when
+ ForbiddenComment:ConformanceTest.kt:ConformanceTest.Companion$// TODO: Fix
+
+
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
index 402cf81..b3331dd 100644
--- 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
@@ -73,6 +73,7 @@ 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
@@ -236,6 +237,7 @@ fun main(args: Array) {
}.start(wait = true)
}
+@Suppress("LongMethod")
private fun createConformanceServer(): Server {
val server = Server(
Implementation(
@@ -371,7 +373,9 @@ private class HttpServerTransport(private val sessionId: String) : AbstractTrans
}
}
- else -> call.respond(HttpStatusCode.Accepted)
+ else -> {
+ call.respond(HttpStatusCode.Accepted)
+ }
}
} catch (e: CancellationException) {
throw e
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
index 7754ff3..41c9c3c 100644
--- 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
@@ -17,6 +17,7 @@ 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 {}
@@ -320,7 +321,8 @@ class ConformanceTest {
}
process.destroyForcibly()
throw AssertionError(
- "❌ $capitalizedType conformance test [$transportType] '$scenario' timed out after $timeoutSeconds seconds",
+ "❌ $capitalizedType conformance test [$transportType] '$scenario' " +
+ "timed out after $timeoutSeconds seconds",
)
}
@@ -331,8 +333,9 @@ class ConformanceTest {
logger.error {
"$capitalizedType conformance test [$transportType] '$scenario' failed with exit code: $exitCode"
}
- throw AssertionError(
- "❌ $capitalizedType conformance test [$transportType] '$scenario' failed (exit code: $exitCode). Check test output above for details.",
+ 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
index f112319..f385ddd 100644
--- 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
@@ -24,6 +24,7 @@ class WebSocketClientTransport(override val session: WebSocketSession) : WebSock
}
}
+@Suppress("LongMethod")
fun main(args: Array) {
require(args.isNotEmpty()) {
"Server WebSocket URL must be provided as an argument"
diff --git a/integration-test/detekt-baseline-commonTestSourceSet.xml b/integration-test/detekt-baseline-commonTestSourceSet.xml
new file mode 100644
index 0000000..660fa66
--- /dev/null
+++ b/integration-test/detekt-baseline-commonTestSourceSet.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ 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
new file mode 100644
index 0000000..46849e9
--- /dev/null
+++ b/integration-test/detekt-baseline-test.xml
@@ -0,0 +1,47 @@
+
+
+
+
+ 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
+ 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
+ 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
+ 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:KotlinTestBase.kt:KotlinTestBase$protected fun setupServer
+ 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
+ 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/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt
index ae760f1..2c15635 100644
--- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt
+++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt
@@ -40,7 +40,7 @@ class InMemoryTransport : AbstractTransport() {
}
override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) {
- val other = otherTransport ?: throw IllegalStateException("Not connected")
+ val other = checkNotNull(otherTransport) { "Not connected" }
other._onMessage.invoke(message)
}
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 fbaca72..9380401 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
@@ -266,7 +266,7 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() {
),
),
) { request ->
- val args = request.params.arguments ?: emptyMap()
+ val args = request.params.arguments.orEmpty()
val arg1 = args["requiredArg1"] ?: throw IllegalArgumentException(
"Missing required argument: requiredArg1",
)
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsNotificationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsNotificationTest.kt
index 3674401..08687d9 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsNotificationTest.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsNotificationTest.kt
@@ -24,8 +24,10 @@ class ServerPromptsNotificationTest : AbstractServerFeaturesTest() {
fun `addPrompt should send notification`() = runTest {
// Configure notification handler
val notifications = mutableListOf()
- client.setNotificationHandler(Method.Defined.NotificationsPromptsListChanged) {
- notifications.add(it)
+ client.setNotificationHandler(
+ Method.Defined.NotificationsPromptsListChanged,
+ ) { notification ->
+ notifications.add(notification)
CompletableDeferred(Unit)
}
@@ -96,8 +98,10 @@ class ServerPromptsNotificationTest : AbstractServerFeaturesTest() {
fun `notification should not be send when removed prompt does not exists`() = runTest {
// Track notifications
val notifications = mutableListOf()
- client.setNotificationHandler(Method.Defined.NotificationsPromptsListChanged) {
- notifications.add(it)
+ client.setNotificationHandler(
+ Method.Defined.NotificationsPromptsListChanged,
+ ) { notification ->
+ notifications.add(notification)
CompletableDeferred(Unit)
}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsTest.kt
index 5f49d04..d7ae46d 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsTest.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsTest.kt
@@ -34,7 +34,7 @@ class ServerPromptsTest : AbstractServerFeaturesTest() {
server.addPrompt(testPrompt) {
GetPromptResult(
description = "Test prompt description",
- messages = listOf(),
+ messages = emptyList(),
)
}
@@ -70,13 +70,13 @@ class ServerPromptsTest : AbstractServerFeaturesTest() {
server.addPrompt(testPrompt1) {
GetPromptResult(
description = "Test prompt description 1",
- messages = listOf(),
+ messages = emptyList(),
)
}
server.addPrompt(testPrompt2) {
GetPromptResult(
description = "Test prompt description 2",
- messages = listOf(),
+ messages = emptyList(),
)
}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationSubscribeTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationSubscribeTest.kt
index d9f9250..06fa962 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationSubscribeTest.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationSubscribeTest.kt
@@ -50,12 +50,12 @@ class ServerResourcesNotificationSubscribeTest : AbstractServerFeaturesTest() {
name = "Test Resource 1",
description = "A test resource 1",
mimeType = "text/plain",
- ) {
+ ) { request ->
ReadResourceResult(
contents = listOf(
TextResourceContents(
text = "Test resource content 1",
- uri = it.uri,
+ uri = request.uri,
mimeType = "text/plain",
),
),
@@ -67,12 +67,12 @@ class ServerResourcesNotificationSubscribeTest : AbstractServerFeaturesTest() {
name = "Test Resource 2",
description = "A test resource 2",
mimeType = "text/plain",
- ) {
+ ) { request ->
ReadResourceResult(
contents = listOf(
TextResourceContents(
text = "Test resource content 2",
- uri = it.uri,
+ uri = request.uri,
mimeType = "text/plain",
),
),
@@ -121,8 +121,8 @@ class ServerResourcesNotificationSubscribeTest : AbstractServerFeaturesTest() {
println("Thread ${Thread.currentThread().name} test 1")
// Track notifications
val notifications = ConcurrentLinkedQueue()
- client.setNotificationHandler(NotificationsResourcesUpdated) {
- notifications.add(it)
+ client.setNotificationHandler(NotificationsResourcesUpdated) { notification ->
+ notifications.add(notification)
CompletableDeferred(Unit)
}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationTest.kt
index 6337479..f007723 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationTest.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesNotificationTest.kt
@@ -26,8 +26,8 @@ class ServerResourcesNotificationTest : AbstractServerFeaturesTest() {
val notifications = mutableListOf()
client.setNotificationHandler(
Method.Defined.NotificationsResourcesListChanged,
- ) {
- notifications.add(it)
+ ) { notification ->
+ notifications.add(notification)
CompletableDeferred(Unit)
}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsNotificationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsNotificationTest.kt
index 4cda75f..3572082 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsNotificationTest.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsNotificationTest.kt
@@ -51,8 +51,10 @@ class ServerToolsNotificationTest : AbstractServerFeaturesTest() {
fun `removeTools should remove multiple tools and send two notifications`() = runTest {
// Configure notification handler
val notifications = mutableListOf()
- client.setNotificationHandler(Method.Defined.NotificationsToolsListChanged) {
- notifications.add(it)
+ client.setNotificationHandler(
+ Method.Defined.NotificationsToolsListChanged,
+ ) { notification ->
+ notifications.add(notification)
CompletableDeferred(Unit)
}
@@ -83,8 +85,10 @@ class ServerToolsNotificationTest : AbstractServerFeaturesTest() {
fun `notification should not be send when removed tool does not exists`() = runTest {
// Track notifications
val notifications = mutableListOf()
- client.setNotificationHandler(Method.Defined.NotificationsToolsListChanged) {
- notifications.add(it)
+ client.setNotificationHandler(
+ Method.Defined.NotificationsToolsListChanged,
+ ) { notification ->
+ notifications.add(notification)
CompletableDeferred(Unit)
}
diff --git a/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml
new file mode 100644
index 0000000..5cddbe1
--- /dev/null
+++ b/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ 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
new file mode 100644
index 0000000..b73eb9f
--- /dev/null
+++ b/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ 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-main.xml b/kotlin-sdk-client/detekt-baseline-main.xml
new file mode 100644
index 0000000..8411ab0
--- /dev/null
+++ b/kotlin-sdk-client/detekt-baseline-main.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ 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.")
+
+
diff --git a/kotlin-sdk-client/detekt-baseline-test.xml b/kotlin-sdk-client/detekt-baseline-test.xml
new file mode 100644
index 0000000..30a384a
--- /dev/null
+++ b/kotlin-sdk-client/detekt-baseline-test.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ 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
+ 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
+ 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 d861bd0..e3added 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
@@ -107,6 +107,7 @@ 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) {
@@ -159,8 +160,8 @@ public open class Client(private val clientInfo: Implementation, options: Client
else -> true
}
- if (!hasCapability) {
- throw IllegalStateException("Server does not support $capability (required for $method)")
+ check(hasCapability) {
+ "Server does not support $capability (required for $method)"
}
}
@@ -184,7 +185,7 @@ public open class Client(private val clientInfo: Implementation, options: Client
val result = request(message)
if (!SUPPORTED_PROTOCOL_VERSIONS.contains(result.protocolVersion)) {
- throw IllegalStateException(
+ error(
"Server's protocol version is not supported: ${result.protocolVersion}",
)
}
@@ -213,8 +214,8 @@ public open class Client(private val clientInfo: Implementation, options: Client
override fun assertCapabilityForMethod(method: Method) {
when (method) {
Method.Defined.LoggingSetLevel -> {
- if (serverCapabilities?.logging == null) {
- throw IllegalStateException("Server does not support logging (required for $method)")
+ checkNotNull(serverCapabilities?.logging) {
+ "Server does not support logging (required for $method)"
}
}
@@ -222,7 +223,7 @@ public open class Client(private val clientInfo: Implementation, options: Client
Method.Defined.PromptsList,
Method.Defined.CompletionComplete,
-> {
- if (serverCapabilities?.prompts == null) {
+ checkNotNull(serverCapabilities?.prompts) {
throw IllegalStateException("Server does not support prompts (required for $method)")
}
}
@@ -244,8 +245,8 @@ public open class Client(private val clientInfo: Implementation, options: Client
}
Method.Defined.ToolsCall, Method.Defined.ToolsList -> {
- if (serverCapabilities?.tools == null) {
- throw IllegalStateException("Server does not support tools (required for $method)")
+ checkNotNull(serverCapabilities?.tools) {
+ "Server does not support tools (required for $method)"
}
}
@@ -262,10 +263,8 @@ public open class Client(private val clientInfo: Implementation, options: Client
override fun assertNotificationCapability(method: Method) {
when (method) {
Method.Defined.NotificationsRootsListChanged -> {
- if (capabilities.roots?.listChanged != true) {
- throw IllegalStateException(
- "Client does not support roots list changed notifications (required for $method)",
- )
+ check(capabilities.roots?.listChanged == true) {
+ "Client does not support roots list changed notifications (required for $method)"
}
}
@@ -285,26 +284,20 @@ public open class Client(private val clientInfo: Implementation, options: Client
override fun assertRequestHandlerCapability(method: Method) {
when (method) {
Method.Defined.SamplingCreateMessage -> {
- if (capabilities.sampling == null) {
- throw IllegalStateException(
- "Client does not support sampling capability (required for $method)",
- )
+ checkNotNull(capabilities.sampling) {
+ "Client does not support sampling capability (required for $method)"
}
}
Method.Defined.RootsList -> {
- if (capabilities.roots == null) {
- throw IllegalStateException(
- "Client does not support roots capability (required for $method)",
- )
+ checkNotNull(capabilities.roots) {
+ "Client does not support roots capability (required for $method)"
}
}
Method.Defined.ElicitationCreate -> {
- if (capabilities.elicitation == null) {
- throw IllegalStateException(
- "Client does not support elicitation capability (required for $method)",
- )
+ checkNotNull(capabilities.elicitation) {
+ "Client does not support elicitation capability (required for $method)"
}
}
@@ -494,9 +487,9 @@ public open class Client(private val clientInfo: Implementation, options: Client
* @throws IllegalStateException If the client does not support roots.
*/
public fun addRoot(uri: String, name: String) {
- if (capabilities.roots == null) {
+ checkNotNull(capabilities.roots) {
logger.error { "Failed to add root '$name': Client does not support roots capability" }
- throw IllegalStateException("Client does not support roots capability.")
+ "Client does not support roots capability."
}
logger.info { "Adding root: $name ($uri)" }
roots.update { current -> current.put(uri, Root(uri, name)) }
@@ -509,9 +502,9 @@ public open class Client(private val clientInfo: Implementation, options: Client
* @throws IllegalStateException If the client does not support roots.
*/
public fun addRoots(rootsToAdd: List) {
- if (capabilities.roots == null) {
+ checkNotNull(capabilities.roots) {
logger.error { "Failed to add roots: Client does not support roots capability" }
- throw IllegalStateException("Client does not support roots capability.")
+ "Client does not support roots capability."
}
logger.info { "Adding ${rootsToAdd.size} roots" }
roots.update { current -> current.putAll(rootsToAdd.associateBy { it.uri }) }
@@ -550,9 +543,9 @@ public open class Client(private val clientInfo: Implementation, options: Client
* @throws IllegalStateException If the client does not support roots.
*/
public fun removeRoots(uris: List): Int {
- if (capabilities.roots == null) {
+ checkNotNull(capabilities.roots) {
logger.error { "Failed to remove roots: Client does not support roots capability" }
- throw IllegalStateException("Client does not support roots capability.")
+ "Client does not support roots capability."
}
logger.info { "Removing ${uris.size} roots" }
@@ -587,9 +580,9 @@ public open class Client(private val clientInfo: Implementation, options: Client
* @throws IllegalStateException if the client does not support elicitation.
*/
public fun setElicitationHandler(handler: (ElicitRequest) -> ElicitResult) {
- if (capabilities.elicitation == null) {
+ checkNotNull(capabilities.elicitation) {
logger.error { "Failed to set elicitation handler: Client does not support elicitation" }
- throw IllegalStateException("Client does not support elicitation.")
+ "Client does not support elicitation."
}
logger.info { "Setting the elicitation handler" }
@@ -600,7 +593,7 @@ public open class Client(private val clientInfo: Implementation, options: Client
// --- Internal Handlers ---
- private suspend fun handleListRoots(): ListRootsResult {
+ private fun handleListRoots(): ListRootsResult {
val rootList = roots.value.values.toList()
return ListRootsResult(rootList)
}
@@ -634,7 +627,8 @@ public open class Client(private val clientInfo: Implementation, options: Client
val labels = it.split('.')
require(labels.all { label -> label.matches(labelPattern) }) {
- "Invalid _meta key '$key': prefix labels must start with a letter, end with letter/digit, and contain only letters, digits, or hyphens"
+ "Invalid _meta key '$key': prefix labels must start with a letter, end with letter/digit, " +
+ "and contain only letters, digits, or hyphens"
}
require(
@@ -649,7 +643,8 @@ public open class Client(private val clientInfo: Implementation, options: Client
// Validate name (empty allowed)
require(name.isEmpty() || name.matches(namePattern)) {
- "Invalid _meta key '$key': name must start and end with alphanumeric characters, and contain only alphanumerics, hyphens, underscores, or dots"
+ "Invalid _meta key '$key': name must start and end with alphanumeric characters, " +
+ "and contain only alphanumerics, hyphens, underscores, or dots"
}
}
}
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 fb1c640..efa5832 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
@@ -85,6 +85,7 @@ public class StreamableHttpClientTransport(
/**
* Sends a single message with optional resumption support
*/
+ @Suppress("ReturnCount", "CyclomaticComplexMethod")
override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) {
logger.debug { "Client sending message via POST to $url: ${McpJson.encodeToString(message)}" }
@@ -259,6 +260,7 @@ public class StreamableHttpClientTransport(
}
}
+ @Suppress("TooGenericExceptionCaught")
private suspend fun collectSse(
session: ClientSSESession,
replayMessageId: RequestId?,
@@ -295,6 +297,7 @@ public class StreamableHttpClientTransport(
}
}
+ @Suppress("CyclomaticComplexMethod")
private suspend fun handleInlineSse(
response: HttpResponse,
replayMessageId: RequestId?,
@@ -331,6 +334,7 @@ public class StreamableHttpClientTransport(
}
}
+ @Suppress("LoopWithTooManyJumpStatements")
while (!channel.isClosedForRead) {
val line = channel.readUTF8Line() ?: break
if (line.isEmpty()) {
diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt
index ec3f947..c02d68e 100644
--- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt
+++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt
@@ -26,8 +26,8 @@ public class WebSocketClientTransport(
override suspend fun initializeSession() {
logger.debug { "Websocket session initialization started..." }
- session = urlString?.let {
- client.webSocketSession(it) {
+ session = urlString?.let { url ->
+ client.webSocketSession(url) {
requestBuilder()
header(HttpHeaders.SecWebSocketProtocol, MCP_SUBPROTOCOL)
diff --git a/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml
new file mode 100644
index 0000000..d774a5b
--- /dev/null
+++ b/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ 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-commonTestSourceSet.xml b/kotlin-sdk-core/detekt-baseline-commonTestSourceSet.xml
new file mode 100644
index 0000000..0a4c838
--- /dev/null
+++ b/kotlin-sdk-core/detekt-baseline-commonTestSourceSet.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ EmptyFunctionBlock:ProtocolTest.kt:RecordingTransport${}
+ EmptyFunctionBlock:ProtocolTest.kt:TestProtocol${}
+ LongMethod:ElicitationTest.kt:ElicitationTest$@Test fun `should serialize ElicitRequest with requested schema`
+ LongMethod:SamplingDslTest.kt:SamplingDslTest$@Test fun `buildCreateMessageRequest should build with all fields`
+ LongMethod:SamplingTest.kt:SamplingTest$@Test fun `should serialize CreateMessageRequest with all fields`
+ LongMethod:ToolsTest.kt:ToolsTest$@Test fun `should serialize Tool with annotations and schemas`
+
+
diff --git a/kotlin-sdk-core/detekt-baseline-main.xml b/kotlin-sdk-core/detekt-baseline-main.xml
new file mode 100644
index 0000000..a22fbcd
--- /dev/null
+++ b/kotlin-sdk-core/detekt-baseline-main.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ 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/detekt-baseline-test.xml b/kotlin-sdk-core/detekt-baseline-test.xml
new file mode 100644
index 0000000..0a4c838
--- /dev/null
+++ b/kotlin-sdk-core/detekt-baseline-test.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ EmptyFunctionBlock:ProtocolTest.kt:RecordingTransport${}
+ EmptyFunctionBlock:ProtocolTest.kt:TestProtocol${}
+ LongMethod:ElicitationTest.kt:ElicitationTest$@Test fun `should serialize ElicitRequest with requested schema`
+ LongMethod:SamplingDslTest.kt:SamplingDslTest$@Test fun `buildCreateMessageRequest should build with all fields`
+ LongMethod:SamplingTest.kt:SamplingTest$@Test fun `should serialize CreateMessageRequest with all fields`
+ LongMethod:ToolsTest.kt:ToolsTest$@Test fun `should serialize Tool with annotations and schemas`
+
+
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 3447f18..1b43482 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
@@ -1,16 +1,14 @@
-@file:Suppress("FunctionName")
+@file:Suppress("FunctionName", "TooManyFunctions")
package io.modelcontextprotocol.kotlin.sdk
-import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities.Roots
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 kotlin.concurrent.atomics.AtomicLong
-import kotlin.concurrent.atomics.ExperimentalAtomicApi
+import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities.Roots as TypesRoots
@Deprecated(
message = "Use `LATEST_PROTOCOL_VERSION` instead",
@@ -40,14 +38,6 @@ public val SUPPORTED_PROTOCOL_VERSIONS: List =
)
public const val JSONRPC_VERSION: String = io.modelcontextprotocol.kotlin.sdk.types.JSONRPC_VERSION
-@OptIn(ExperimentalAtomicApi::class)
-@Deprecated(
- message = "Use `REQUEST_MESSAGE_ID` instead",
- replaceWith = ReplaceWith("REQUEST_MESSAGE_ID", "io.modelcontextprotocol.kotlin.sdk.types.REQUEST_MESSAGE_ID"),
- level = DeprecationLevel.ERROR,
-)
-private val REQUEST_MESSAGE_ID: AtomicLong = AtomicLong(0L)
-
/**
* A progress token, used to associate progress notifications with the original request.
* Stores message ID.
@@ -81,8 +71,6 @@ public typealias WithMeta = io.modelcontextprotocol.kotlin.sdk.types.WithMeta
/**
* An implementation of [WithMeta] containing custom metadata.
- *
- * @param _meta The JSON object holding metadata. Defaults to an empty JSON object.
*/
@Deprecated(
message = "This class will be removed. Use `WithMeta` instead",
@@ -431,8 +419,6 @@ internal fun JSONRPCRequest.fromJSON(): Request = this.fromJSON()
/**
* A custom request with a specified method.
- *
- * @param method The method associated with the request.
*/
@Deprecated(
message = "Use `CustomRequest` instead",
@@ -489,8 +475,6 @@ public typealias RequestResult = io.modelcontextprotocol.kotlin.sdk.types.Reques
/**
* An empty result for a request containing optional metadata.
- *
- * @param _meta Additional metadata for the response. Defaults to an empty JSON object.
*/
@Deprecated(
message = "Use `EmptyResult` instead",
@@ -708,7 +692,8 @@ public object CancelledNotification {
/**
* 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.
+ * 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.
*
@@ -745,6 +730,15 @@ public typealias Implementation = io.modelcontextprotocol.kotlin.sdk.types.Imple
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(
@@ -753,8 +747,7 @@ public object ClientCapabilities {
),
level = DeprecationLevel.ERROR,
)
- public fun Roots(listChanged: Boolean? = null): io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities.Roots =
- io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities.Roots(listChanged)
+ public fun Roots(listChanged: Boolean? = null): TypesRoots = TypesRoots(listChanged)
}
/**
@@ -769,7 +762,7 @@ public object ClientCapabilities {
)
public fun ClientCapabilities(
sampling: JsonObject? = null,
- roots: Roots? = null,
+ roots: TypesRoots? = null,
elicitation: JsonObject? = null,
experimental: JsonObject? = null,
): io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities =
@@ -918,6 +911,7 @@ public object ServerCapabilities {
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,
@@ -1027,9 +1021,6 @@ public sealed interface ProgressBase {
/**
* Represents a progress notification.
- *
- * @property progress The current progress value.
- * @property total The total progress required, if known.
*/
@Deprecated(
message = "Use `Progress` instead",
@@ -1117,8 +1108,6 @@ public typealias ResourceContents = io.modelcontextprotocol.kotlin.sdk.types.Res
/**
* Represents the text contents of a resource.
- *
- * @property text The text of the item. This must only be set if the item can actually be represented as text (not binary data).
*/
@Deprecated(
message = "Use `TextResourceContents` instead",
@@ -1129,8 +1118,6 @@ public typealias TextResourceContents = io.modelcontextprotocol.kotlin.sdk.types
/**
* Represents the binary contents of a resource encoded as a base64 string.
- *
- * @property blob A base64-encoded string representing the binary data of the item.
*/
@Deprecated(
message = "Use `BlobResourceContents` instead",
@@ -1278,7 +1265,8 @@ public fun 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.
+ * Sent from the client to request resources/updated notifications from the server
+ * whenever a particular resource changes.
*/
@Deprecated(
message = "Use `SubscribeRequest` instead",
@@ -1288,7 +1276,8 @@ public fun ResourceListChangedNotification(
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.
+ * 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",
@@ -1322,7 +1311,8 @@ public object ResourceUpdatedNotification {
}
/**
- * 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.
+ * 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",
@@ -1397,7 +1387,7 @@ public typealias GetPromptRequest = io.modelcontextprotocol.kotlin.sdk.types.Get
replaceWith = ReplaceWith("ContentBlock"),
level = DeprecationLevel.ERROR,
)
-public typealias PromptMessageContent = io.modelcontextprotocol.kotlin.sdk.types.ContentBlock
+public typealias PromptMessageContent = ContentBlock
/**
* Represents prompt message content that is either text, image or audio.
@@ -1610,6 +1600,7 @@ public object Tool {
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,
@@ -1698,7 +1689,7 @@ public fun CallToolResult(
replaceWith = ReplaceWith("ToolCallResult"),
level = DeprecationLevel.ERROR,
)
-@Suppress("DEPRECATION_ERROR")
+@Suppress("DEPRECATION_ERROR", "ConstructorParameterNaming")
public class CompatibilityCallToolResult(
public val content: List,
public val structuredContent: JsonObject? = null,
@@ -2250,10 +2241,6 @@ public fun CreateElicitationResult(
/**
* Represents an error specific to the MCP protocol.
- *
- * @property code The error code.
- * @property message The error message.
- * @property data Additional error data as a JSON object.
*/
@Deprecated(
message = "Use `McpException` instead",
diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.dsl.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.dsl.kt
index bbcfe6c..29d3c99 100644
--- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.dsl.kt
+++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.dsl.kt
@@ -160,7 +160,8 @@ public class CompleteRequestBuilder @PublishedApi internal constructor() : Reque
}
val reference = requireNotNull(ref) {
- "Missing required field 'ref(Reference)'. Use ref(PromptReference(\"name\")) or ref(ResourceTemplateReference(\"uri\"))"
+ "Missing required field 'ref(Reference)'. Use ref(PromptReference(\"name\")) " +
+ "or ref(ResourceTemplateReference(\"uri\"))"
}
val params = CompleteRequestParams(argument = argument, ref = reference, context = ctx, meta = meta)
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 50fd02f..68b2f02 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
@@ -16,8 +16,22 @@ import kotlin.uuid.Uuid
public const val JSONRPC_VERSION: String = "2.0"
+/**
+ * Creates a `RequestId` instance using the provided string value.
+ *
+ * @param value The string representation of the request ID. This value is used
+ * to uniquely identify a request in the JSON-RPC context.
+ * @return A `RequestId` instance of type `StringId` with the provided string value.
+ */
public fun RequestId(value: String): RequestId = RequestId.StringId(value)
+/**
+ * Creates a `RequestId` instance using the given numeric value.
+ *
+ * @param value The numeric value to be used for the `RequestId`. This should uniquely
+ * identify a request when represented as a `NumberId`.
+ * @return A `RequestId.NumberId` instance encapsulating the provided numeric value.
+ */
public fun RequestId(value: Long): RequestId = RequestId.NumberId(value)
/**
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 b9fce4a..566905f 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
@@ -24,6 +24,17 @@ public val McpJson: Json by lazy {
}
}
+/**
+ * Converts a [Map] with [String] keys and nullable [Any] values into a [Map] with
+ * [String] keys and [JsonElement] values.
+ *
+ * Each value in the resulting map is derived from the corresponding value
+ * in the input map, transformed into a [JsonElement]. If the transformation
+ * fails for any reason, the value is replaced with a [JsonPrimitive] containing
+ * the original value's `toString` representation.
+ *
+ * @return A new map where the values are converted to [JsonElement] instances.
+ */
public fun Map.toJson(): Map = this.mapValues { (_, value) ->
runCatching { convertToJsonElement(value) }
.getOrElse { JsonPrimitive(value.toString()) }
@@ -77,7 +88,5 @@ private fun convertToJsonElement(value: Any?): JsonElement = when (value) {
is UByteArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } }
- else -> {
- JsonPrimitive(value.toString())
- }
+ else -> JsonPrimitive(value.toString())
}
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 9270884..8eb4e4b 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
@@ -262,7 +262,8 @@ public data class PromptListChangedNotification(override val params: BaseNotific
// ============================================================================
/**
- * An optional notification from the server to the client, informing it that the list of resources it can read from has changed.
+ * 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.
* Sent only if the server's [ServerCapabilities.resources] has `listChanged = true`.
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 7e10204..699dfce 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
@@ -22,6 +22,12 @@ public value class RequestMeta(public val json: JsonObject) {
}
}
+ /**
+ * Retrieves the value associated with the specified key from the JSON object.
+ *
+ * @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]
}
diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt
index 9341265..c42589e 100644
--- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt
+++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt
@@ -1,6 +1,7 @@
package io.modelcontextprotocol.kotlin.sdk.types
import io.kotest.assertions.json.shouldEqualJson
+import io.kotest.matchers.shouldBe
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.double
import kotlinx.serialization.json.jsonPrimitive
@@ -149,7 +150,9 @@ class RequestTest {
val method = custom.method
val customMethod = assertIs(method)
assertEquals("extensions/customAction", customMethod.value)
- assertEquals("custom-1", custom.params?.meta?.json?.get("progressToken")?.jsonPrimitive?.content)
+ custom.params?.meta?.json
+ ?.get("progressToken")
+ ?.jsonPrimitive?.content shouldBe "custom-1"
}
@Test
diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PingRequestDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PingRequestDslTest.kt
index 5a219ba..79cef06 100644
--- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PingRequestDslTest.kt
+++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/PingRequestDslTest.kt
@@ -49,12 +49,16 @@ class PingRequestDslTest {
val requestInt = buildPingRequest {
meta { progressToken(123) }
}
- requestInt.params?.meta?.json?.get("progressToken")?.jsonPrimitive?.int shouldBe 123
+ requestInt.params?.meta?.json
+ ?.get("progressToken")
+ ?.jsonPrimitive?.int shouldBe 123
val requestLong = buildPingRequest {
meta { progressToken(456L) }
}
- requestLong.params?.meta?.json?.get("progressToken")?.jsonPrimitive?.int shouldBe 456
+ requestLong.params?.meta?.json
+ ?.get("progressToken")
+ ?.jsonPrimitive?.int shouldBe 456
}
@Test
diff --git a/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml
new file mode 100644
index 0000000..67d5e73
--- /dev/null
+++ b/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ 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$*
+ MaxLineLength:ServerSession.kt:ServerSession$"Client requested unsupported protocol version $requestedVersion, falling back to $LATEST_PROTOCOL_VERSION"
+ MaxLineLength:ServerSession.kt:ServerSession$"Creating message with ${params.params.messages.size} messages, maxTokens=${params.params.maxTokens}, temperature=${params.params.temperature}, systemPrompt=${if (params.params.systemPrompt != null) "present" else "absent"}"
+ ReturnCount:KtorServer.kt:private suspend fun existingStreamableTransport: StreamableHttpServerTransport?
+ ThrowsCount:ServerSession.kt:ServerSession$override fun assertCapabilityForMethod
+ ThrowsCount:ServerSession.kt:ServerSession$override fun assertNotificationCapability
+ ThrowsCount:ServerSession.kt:ServerSession$override fun assertRequestHandlerCapability
+ TooGenericExceptionCaught:SSEServerTransport.kt:SseServerTransport$e: Exception
+ TooGenericExceptionCaught:Server.kt:Server$e: Exception
+ TooGenericExceptionCaught:StdioServerTransport.kt:StdioServerTransport$e: Throwable
+ TooManyFunctions:KtorServer.kt:io.modelcontextprotocol.kotlin.sdk.server.KtorServer.kt
+ TooManyFunctions:Server.kt:Server
+ TooManyFunctions:ServerSession.kt:ServerSession : Protocol
+
+
diff --git a/kotlin-sdk-server/detekt-baseline-main.xml b/kotlin-sdk-server/detekt-baseline-main.xml
new file mode 100644
index 0000000..c016bed
--- /dev/null
+++ b/kotlin-sdk-server/detekt-baseline-main.xml
@@ -0,0 +1,51 @@
+
+
+
+
+ InjectDispatcher:FeatureNotificationService.kt:FeatureNotificationService$Default
+ LongParameterList:KtorServer.kt:private suspend fun RoutingContext.streamableTransport: StreamableHttpServerTransport?
+ LongParameterList:Server.kt:Server$public fun addTool
+ 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$*
+ MaxLineLength:ServerSession.kt:ServerSession$"Client requested unsupported protocol version $requestedVersion, falling back to $LATEST_PROTOCOL_VERSION"
+ MaxLineLength:ServerSession.kt:ServerSession$"Creating message with ${params.params.messages.size} messages, maxTokens=${params.params.maxTokens}, temperature=${params.params.temperature}, systemPrompt=${if (params.params.systemPrompt != null) "present" else "absent"}"
+ NoNameShadowing:FeatureNotificationService.kt:FeatureNotificationService${ it.remove(job) }
+ RedundantSuspendModifier:ServerSession.kt:ServerSession$suspend
+ ReturnCount:KtorServer.kt:private suspend fun existingStreamableTransport: StreamableHttpServerTransport?
+ ThrowsCount:ServerSession.kt:ServerSession$override fun assertCapabilityForMethod
+ ThrowsCount:ServerSession.kt:ServerSession$override fun assertNotificationCapability
+ ThrowsCount:ServerSession.kt:ServerSession$override fun assertRequestHandlerCapability
+ TooGenericExceptionCaught:SSEServerTransport.kt:SseServerTransport$e: Exception
+ TooGenericExceptionCaught:Server.kt:Server$e: Exception
+ TooGenericExceptionCaught:StdioServerTransport.kt:StdioServerTransport$e: Throwable
+ TooManyFunctions:KtorServer.kt:io.modelcontextprotocol.kotlin.sdk.server.KtorServer.kt
+ TooManyFunctions:Server.kt:Server
+ TooManyFunctions:ServerSession.kt:ServerSession : Protocol
+ 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." }
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException( "Server does not support notifying about resources (required for ${method.value})", )
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException( "Server does not support notifying of prompt list changes (required for ${method.value})", )
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException( "Server does not support notifying of tool list changes (required for ${method.value})", )
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Client does not support elicitation (required for ${method.value})")
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Client does not support listing roots (required for ${method.value})")
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Client does not support sampling (required for ${method.value})")
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support logging (required for $method)")
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support logging (required for ${method.value})")
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support prompts (required for $method)")
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support resources (required for $method)")
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support sampling (required for $method)")
+ UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support tools (required for $method)")
+
+
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 8bb09a8..90aee6b 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
@@ -46,6 +46,12 @@ internal class TransportManager(transports: Map = emp
}
}
+/**
+ * Registers a server-sent events (SSE) route at the specified path.
+ *
+ * @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.
+ */
@KtorDsl
public fun Routing.mcp(path: String, block: ServerSSESession.() -> Server) {
route(path) {
@@ -53,7 +59,7 @@ public fun Routing.mcp(path: String, block: ServerSSESession.() -> Server) {
}
}
-/*
+/**
* Configures the Ktor Application to handle Model Context Protocol (MCP) over Server-Sent Events (SSE).
*/
@KtorDsl
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 bd683af..1894cdc 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
@@ -25,6 +25,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse
import io.modelcontextprotocol.kotlin.sdk.types.McpJson
import io.modelcontextprotocol.kotlin.sdk.types.Method
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.job
@@ -66,20 +67,23 @@ private data class SessionContext(val session: ServerSSESession?, val call: Appl
* - 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.
+ * 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.
+ * 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.
+ * 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).
+ * 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).
*/
@OptIn(ExperimentalUuidApi::class, ExperimentalAtomicApi::class)
+@Suppress("TooManyFunctions")
public class StreamableHttpServerTransport(
private val enableJsonResponse: Boolean = false,
private val enableDnsRebindingProtection: Boolean = false,
@@ -145,10 +149,12 @@ public class StreamableHttpServerTransport(
override suspend fun start() {
check(started.compareAndSet(expectedValue = false, newValue = true)) {
- "StreamableHttpServerTransport already started! If using Server class, note that connect() calls start() automatically."
+ "StreamableHttpServerTransport already started! If using Server class, " +
+ "note that connect() calls start() automatically."
}
}
+ @Suppress("CyclomaticComplexMethod", "ReturnCount")
override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) {
val responseRequestId: RequestId? = when (message) {
is JSONRPCResponse -> message.id
@@ -252,6 +258,7 @@ public class StreamableHttpServerTransport(
/**
* Handles POST requests containing JSON-RPC messages
*/
+ @Suppress("CyclomaticComplexMethod", "LongMethod", "ReturnCount", "TooGenericExceptionCaught")
public suspend fun handlePostRequest(session: ServerSSESession?, call: ApplicationCall) {
try {
if (!enableJsonResponse && session == null) {
@@ -344,6 +351,7 @@ public class StreamableHttpServerTransport(
}
}
+ @Suppress("ReturnCount")
public suspend fun handleGetRequest(session: ServerSSESession?, call: ApplicationCall) {
if (enableJsonResponse) {
call.reject(
@@ -403,6 +411,7 @@ public class StreamableHttpServerTransport(
* 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 (enableJsonResponse) return
val streamId = requestToStreamMapping[requestId] ?: return
@@ -417,6 +426,7 @@ public class StreamableHttpServerTransport(
}
}
+ @Suppress("TooGenericExceptionCaught")
private suspend fun replayEvents(store: EventStore, lastEventId: String, session: ServerSSESession) {
val call: ApplicationCall = session.call
@@ -479,6 +489,7 @@ public class StreamableHttpServerTransport(
}
}
+ @Suppress("ReturnCount")
private suspend fun validateSession(call: ApplicationCall): Boolean {
if (sessionIdGenerator == null) return true
@@ -519,7 +530,7 @@ public class StreamableHttpServerTransport(
else -> {
call.reject(
HttpStatusCode.NotFound,
- -32001,
+ REQUEST_TIMEOUT,
"Session not found",
)
false
@@ -549,6 +560,7 @@ public class StreamableHttpServerTransport(
}
}
+ @Suppress("ReturnCount")
private fun validateHeaders(call: ApplicationCall): String? {
if (!enableDnsRebindingProtection) return null
@@ -573,6 +585,7 @@ public class StreamableHttpServerTransport(
return null
}
+ @Suppress("TooGenericExceptionCaught")
private suspend fun flushSse(session: ServerSSESession?) {
try {
session?.send(data = "")
@@ -581,6 +594,7 @@ public class StreamableHttpServerTransport(
}
}
+ @Suppress("ReturnCount", "MagicNumber")
private suspend fun parseBody(call: ApplicationCall): List? {
val contentLength = call.request.header(HttpHeaders.ContentLength)?.toIntOrNull() ?: 0
if (contentLength > MAXIMUM_MESSAGE_SIZE) {
@@ -630,6 +644,7 @@ public class StreamableHttpServerTransport(
}
}
+ @Suppress("TooGenericExceptionCaught")
private suspend fun maybeSendPrimingEvent(streamId: String, session: ServerSSESession?) {
val store = eventStore ?: return
val sseSession = session ?: return
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 b4d8849..edd54d3 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,3 +1,5 @@
+@file:Suppress("TooManyFunctions")
+
package io.modelcontextprotocol.kotlin.sdk.server
import io.github.oshai.kotlinlogging.KotlinLogging
diff --git a/test-utils/detekt-baseline.xml b/test-utils/detekt-baseline.xml
new file mode 100644
index 0000000..390ccd0
--- /dev/null
+++ b/test-utils/detekt-baseline.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ CyclomaticComplexMethod:Retry.kt:RetryExtension$private fun executeWithRetry
+ ReturnCount:TypeScriptRunner.kt:TypeScriptRunner$public fun installDependencies
+ ThrowsCount:Retry.kt:RetryExtension$private fun executeWithRetry
+
+
diff --git a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt b/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt
index 3aa1de7..ff9292e 100644
--- a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt
+++ b/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt
@@ -1,5 +1,6 @@
package io.modelcontextprotocol.kotlin.test.utils
+import org.junit.jupiter.api.fail
import java.io.File
/**
@@ -83,7 +84,7 @@ public object TypeScriptRunner {
patchPackageJsonRecursively(typescriptDir, patchProtocols = true)
if (tryCommand(listOf("npm", "install"), typescriptDir)) return
- throw RuntimeException("Failed to install TypeScript dependencies in ${typescriptDir.absolutePath}")
+ fail("Failed to install TypeScript dependencies in ${typescriptDir.absolutePath}")
}
private fun tryCommand(command: List, workingDir: File): Boolean {
diff --git a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt b/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt
index a80c825..71fcfa6 100644
--- a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt
+++ b/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt
@@ -14,6 +14,7 @@ import kotlin.time.Duration.Companion.seconds
*/
public fun killProcessOnPort(port: Int) {
val killCommand = if (isWindows) {
+ @Suppress("MaxLineLength")
listOf(
"cmd.exe",
"/c",