refactor: Harden detekt setup, address some issues (#507)

# Detekt setup hardeing; address some issues

- Address some detekt findings: missing KDocs, formatting, use `check*`
& `orEmpty()``
- Remove unnecessary KDoc comments from deprecated classes in `types.kt`
- Add Detekt baseline XML files for all modules
- Enforce stricter Detekt rules by setting `failOnSeverity` from `None`
to `Error`

## Motivation and Context

Detekt is not actually failing the build on violations. This PR is
enabling Detekt to fail on errors. Baseline fines added for historical
issues. Follow-up for #501

## How Has This Been Tested?
Regressions

## Breaking Changes

No breaking changes.

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Refactoring
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed
This commit is contained in:
Konstantin Pavlov 2026-02-13 22:17:27 +02:00 • committed by GitHub
commit ed9ad83290
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 478 additions and 122 deletions

View file

@ -26,7 +26,7 @@ subprojects {
detekt {
config = files("$rootDir/config/detekt/detekt.yml")
buildUponDefaultConfig = true
failOnSeverity.set(FailOnSeverity.Never)
failOnSeverity.set(FailOnSeverity.Error)
}
}

View file

@ -0,0 +1,9 @@
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BracesOnWhenStatements:ConformanceServer.kt:HttpServerTransport$when</ID>
<ID>BracesOnWhenStatements:ConformanceTest.kt:ConformanceTest$when</ID>
<ID>ForbiddenComment:ConformanceTest.kt:ConformanceTest.Companion$// TODO: Fix</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -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<String>) {
val port = args.getOrNull(0)?.toIntOrNull() ?: 3000
@ -236,6 +237,7 @@ fun main(args: Array<String>) {
}.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

View file

@ -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.",
)
}
}

View file

@ -24,6 +24,7 @@ class WebSocketClientTransport(override val session: WebSocketSession) : WebSock
}
}
@Suppress("LongMethod")
fun main(args: Array<String>) {
require(args.isNotEmpty()) {
"Server WebSocket URL must be provided as an argument"

View file

@ -0,0 +1,11 @@
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>EmptyFunctionBlock:ClientTest.kt:ClientTest.&lt;no name provided>${ }</ID>
<ID>EmptyFunctionBlock:ClientTest.kt:ClientTest.&lt;no name provided>${}</ID>
<ID>LargeClass:ClientTest.kt:ClientTest</ID>
<ID>LongMethod:ClientTest.kt:ClientTest$@Test fun `JSONRPCRequest with ToolsList method and default params returns list of tools`</ID>
<ID>LongMethod:ClientTest.kt:ClientTest$@Test fun `should handle logging setLevel request`</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,47 @@
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>AbstractClassCanBeConcreteClass:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$AbstractPromptIntegrationTest</ID>
<ID>AbstractClassCanBeConcreteClass:AbstractResourceIntegrationTest.kt:AbstractResourceIntegrationTest$AbstractResourceIntegrationTest</ID>
<ID>AbstractClassCanBeConcreteClass:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$AbstractToolIntegrationTest</ID>
<ID>AbstractClassCanBeConcreteClass:BaseTransportTest.kt:BaseTransportTest$BaseTransportTest</ID>
<ID>CyclomaticComplexMethod:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$private fun setupCalculatorTool</ID>
<ID>EmptyFunctionBlock:ClientTest.kt:ClientTest.&lt;no name provided>${ }</ID>
<ID>EmptyFunctionBlock:ClientTest.kt:ClientTest.&lt;no name provided>${}</ID>
<ID>ForbiddenComment:StdioClientTransportTest.kt:StdioClientTransportTest$// TODO: fix running on windows</ID>
<ID>InjectDispatcher:AbstractKotlinClientTsServerTest.kt:AbstractKotlinClientTsServerTest$IO</ID>
<ID>InjectDispatcher:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$IO</ID>
<ID>InjectDispatcher:AbstractResourceIntegrationTest.kt:AbstractResourceIntegrationTest$IO</ID>
<ID>InjectDispatcher:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$IO</ID>
<ID>InjectDispatcher:KotlinClientTsServerEdgeCasesTestSse.kt:KotlinClientTsServerEdgeCasesTestSse$IO</ID>
<ID>InjectDispatcher:KotlinClientTsServerEdgeCasesTestStdio.kt:KotlinClientTsServerEdgeCasesTestStdio$IO</ID>
<ID>InjectDispatcher:SseIntegrationTest.kt:SseIntegrationTest$IO</ID>
<ID>InjectDispatcher:StdioClientTransportTest.kt:StdioClientTransportTest$IO</ID>
<ID>InjectDispatcher:StreamableHttpIntegrationTest.kt:StreamableHttpIntegrationTest$IO</ID>
<ID>InjectDispatcher:TsEdgeCasesTestSse.kt:TsEdgeCasesTestSse$IO</ID>
<ID>InjectDispatcher:WebSocketIntegrationTest.kt:WebSocketIntegrationTest$IO</ID>
<ID>LargeClass:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest : KotlinTestBase</ID>
<ID>LargeClass:ClientTest.kt:ClientTest</ID>
<ID>LongMethod:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$@Test fun testMissingRequiredArguments</ID>
<ID>LongMethod:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$override fun configureServer</ID>
<ID>LongMethod:AbstractResourceIntegrationTest.kt:AbstractResourceIntegrationTest$override fun configureServer</ID>
<ID>LongMethod:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$private fun setupCalculatorTool</ID>
<ID>LongMethod:ClientTest.kt:ClientTest$@Test fun `JSONRPCRequest with ToolsList method and default params returns list of tools`</ID>
<ID>LongMethod:ClientTest.kt:ClientTest$@Test fun `should handle logging setLevel request`</ID>
<ID>LongMethod:KotlinServerForTsClientSse.kt:HttpServerTransport$suspend fun handleRequest</ID>
<ID>LongMethod:KotlinServerForTsClientSse.kt:KotlinServerForTsClient$fun createMcpServer: Server</ID>
<ID>LongMethod:KotlinServerForTsClientSse.kt:KotlinServerForTsClient$fun start</ID>
<ID>LongMethod:KotlinTestBase.kt:KotlinTestBase$protected fun setupServer</ID>
<ID>LongMethod:ServerResourcesNotificationSubscribeTest.kt:ServerResourcesNotificationSubscribeTest$@Test fun `should send resource notifications`</ID>
<ID>LongMethod:TsEdgeCasesTestSse.kt:TsEdgeCasesTestSse$@Test @Timeout(30, unit = TimeUnit.SECONDS) fun testComplexConcurrentRequests: Unit</ID>
<ID>MatchingDeclarationName:PromptIntegrationTestSse.kt:SchemaPromptIntegrationTestSse : AbstractPromptIntegrationTest</ID>
<ID>SleepInsteadOfDelay:KotlinServerForTsClientSse.kt:KotlinServerForTsClient$sleep(500)</ID>
<ID>ThrowsCount:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$override fun configureServer</ID>
<ID>UnusedPrivateProperty:KotlinClientTsServerTestSse.kt:KotlinClientTsServerTestSse$private val host = "localhost"</ID>
<ID>UseCheckOrError:ClientTest.kt:ClientTest.&lt;no name provided>$throw IllegalStateException("Test error")</ID>
<ID>UseOrEmpty:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$(request.params.arguments?.get("tags") as? JsonArray)?.mapNotNull { (it as? JsonPrimitive)?.content } ?: emptyList()</ID>
<ID>UseRequire:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$throw IllegalArgumentException("Missing required argument: $argName")</ID>
<ID>VarCouldBeVal:KotlinClientTsServerEdgeCasesTestSse.kt:KotlinClientTsServerEdgeCasesTestSse$private lateinit var client: Client</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -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)
}

View file

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

View file

@ -24,8 +24,10 @@ class ServerPromptsNotificationTest : AbstractServerFeaturesTest() {
fun `addPrompt should send notification`() = runTest {
// Configure notification handler
val notifications = mutableListOf<PromptListChangedNotification>()
client.setNotificationHandler<PromptListChangedNotification>(Method.Defined.NotificationsPromptsListChanged) {
notifications.add(it)
client.setNotificationHandler<PromptListChangedNotification>(
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<PromptListChangedNotification>()
client.setNotificationHandler<PromptListChangedNotification>(Method.Defined.NotificationsPromptsListChanged) {
notifications.add(it)
client.setNotificationHandler<PromptListChangedNotification>(
Method.Defined.NotificationsPromptsListChanged,
) { notification ->
notifications.add(notification)
CompletableDeferred(Unit)
}

View file

@ -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(),
)
}

View file

@ -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<ResourceUpdatedNotification>()
client.setNotificationHandler<ResourceUpdatedNotification>(NotificationsResourcesUpdated) {
notifications.add(it)
client.setNotificationHandler<ResourceUpdatedNotification>(NotificationsResourcesUpdated) { notification ->
notifications.add(notification)
CompletableDeferred(Unit)
}

View file

@ -26,8 +26,8 @@ class ServerResourcesNotificationTest : AbstractServerFeaturesTest() {
val notifications = mutableListOf<ResourceListChangedNotification>()
client.setNotificationHandler<ResourceListChangedNotification>(
Method.Defined.NotificationsResourcesListChanged,
) {
notifications.add(it)
) { notification ->
notifications.add(notification)
CompletableDeferred(Unit)
}

View file

@ -51,8 +51,10 @@ class ServerToolsNotificationTest : AbstractServerFeaturesTest() {
fun `removeTools should remove multiple tools and send two notifications`() = runTest {
// Configure notification handler
val notifications = mutableListOf<ToolListChangedNotification>()
client.setNotificationHandler<ToolListChangedNotification>(Method.Defined.NotificationsToolsListChanged) {
notifications.add(it)
client.setNotificationHandler<ToolListChangedNotification>(
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<ToolListChangedNotification>()
client.setNotificationHandler<ToolListChangedNotification>(Method.Defined.NotificationsToolsListChanged) {
notifications.add(it)
client.setNotificationHandler<ToolListChangedNotification>(
Method.Defined.NotificationsToolsListChanged,
) { notification ->
notifications.add(notification)
CompletableDeferred(Unit)
}

View file

@ -0,0 +1,14 @@
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>CyclomaticComplexMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize</ID>
<ID>LongMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize</ID>
<ID>MaxLineLength:StdioClientTransport.kt:StdioClientTransport$*</ID>
<ID>ThrowsCount:SseClientTransport.kt:SseClientTransport$private suspend fun CoroutineScope.collectMessages</ID>
<ID>ThrowsCount:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$@Suppress("ReturnCount", "CyclomaticComplexMethod") override suspend fun performSend</ID>
<ID>TooGenericExceptionCaught:Client.kt:Client$error: Throwable</ID>
<ID>TooGenericExceptionCaught:SseClientTransport.kt:SseClientTransport$e: Throwable</ID>
<ID>TooGenericExceptionCaught:StdioClientTransport.kt:StdioClientTransport$e: Throwable</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,9 @@
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>LongMethod:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$@Ignore @Test fun testNotificationSchemaE2E</ID>
<ID>MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}"""</ID>
<ID>MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}"""</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,19 @@
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>CyclomaticComplexMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize</ID>
<ID>InjectDispatcher:StdioClientTransport.kt:StdioClientTransport$Default</ID>
<ID>InjectDispatcher:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$Default</ID>
<ID>LongMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize</ID>
<ID>MaxLineLength:StdioClientTransport.kt:StdioClientTransport$*</ID>
<ID>NoNameShadowing:StdioClientTransport.kt:StdioClientTransport$source</ID>
<ID>ThrowsCount:SseClientTransport.kt:SseClientTransport$private suspend fun CoroutineScope.collectMessages</ID>
<ID>ThrowsCount:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$@Suppress("ReturnCount", "CyclomaticComplexMethod") override suspend fun performSend</ID>
<ID>TooGenericExceptionCaught:Client.kt:Client$error: Throwable</ID>
<ID>TooGenericExceptionCaught:SseClientTransport.kt:SseClientTransport$e: Throwable</ID>
<ID>TooGenericExceptionCaught:StdioClientTransport.kt:StdioClientTransport$e: Throwable</ID>
<ID>UseCheckOrError:Client.kt:Client$throw IllegalStateException( "Server does not support resource subscriptions (required for $method)", )</ID>
<ID>UseCheckOrError:Client.kt:Client$throw IllegalStateException("Client does not support roots capability.")</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,18 @@
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>AbstractClassCanBeConcreteClass:AbstractStreamableHttpClientTest.kt:AbstractStreamableHttpClientTest$AbstractStreamableHttpClientTest</ID>
<ID>ForbiddenComment:StreamableHttpClientTest.kt:StreamableHttpClientTest$// TODO: how to get notifications via Client API?</ID>
<ID>InjectDispatcher:StdioClientTransportErrorHandlingTest.kt:StdioClientTransportErrorHandlingTest$IO</ID>
<ID>InjectDispatcher:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$Default</ID>
<ID>LongMethod:StreamableHttpClientTest.kt:StreamableHttpClientTest$@Test fun `test streamableHttpClient`</ID>
<ID>LongMethod:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$@Ignore @Test fun testNotificationSchemaE2E</ID>
<ID>LongParameterList:MockMcp.kt:MockMcp$fun handleJSONRPCRequest</ID>
<ID>LongParameterList:MockMcp.kt:MockMcp$fun handleWithResult</ID>
<ID>LongParameterList:MockMcp.kt:MockMcp$fun onInitialize</ID>
<ID>MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}"""</ID>
<ID>MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}"""</ID>
<ID>UseOrEmpty:ClientMetaParameterTest.kt:ClientMetaParameterTest$exception.message ?: ""</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -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<InitializeResult>(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<Root>) {
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<String>): 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"
}
}
}

View file

@ -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()) {

Some files were not shown because too many files have changed in this diff Show more