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",