From 64804a05c227389440f5a4c6cf2712f527b54b77 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 29 Apr 2026 12:31:49 +0200 Subject: [PATCH] refactor: explicit locked detekt config (#729) Migrate detekt to a convention plugin in `buildSrc`, switch the project to a fully explicit YAML config (`buildUponDefaultConfig = false`), fix the real findings that this stricter setup revealed, and remove `@Suppress` annotations that became unnecessary after the rule changes. ## Motivation and Context - Explicit visibility into the rule set - Lock against detekt version upgrades ## Breaking Changes None ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed --- .claude/skills/kdoc/SKILL.md | 77 +- .github/workflows/build.yml | 2 +- build.gradle.kts | 13 - buildSrc/build.gradle.kts | 1 + .../src/main/kotlin/mcp.detekt.gradle.kts | 23 + .../main/kotlin/mcp.multiplatform.gradle.kts | 9 - config/detekt/detekt.yml | 869 +++++++++++++++++- .../sdk/conformance/ConformanceTools.kt | 1 - gradle/libs.versions.toml | 4 +- integration-test/build.gradle.kts | 1 + integration-test/detekt-baseline-test.xml | 17 - kotlin-sdk-client/build.gradle.kts | 1 + .../detekt-baseline-commonMainSourceSet.xml | 14 - .../detekt-baseline-commonTestSourceSet.xml | 8 - kotlin-sdk-client/detekt-baseline-main.xml | 14 +- kotlin-sdk-client/detekt-baseline-test.xml | 14 - .../kotlin/sdk/client/Client.kt | 19 +- .../kotlin/sdk/client/StdioClientTransport.kt | 3 +- .../client/StreamableHttpClientTransport.kt | 25 +- .../http/StreamableHttpClientTransportTest.kt | 1 - .../kotlin/sdk/client/MockMcp.kt | 4 - .../sdk/client/StreamableHttpClientTest.kt | 4 +- kotlin-sdk-core/build.gradle.kts | 1 + .../detekt-baseline-commonMainSourceSet.xml | 27 - kotlin-sdk-core/detekt-baseline-main.xml | 25 - .../sdk/shared/AbstractClientTransport.kt | 6 +- .../kotlin/sdk/shared/AbstractTransport.kt | 8 +- .../kotlin/sdk/shared/Protocol.kt | 61 +- .../kotlin/sdk/shared/ReadBuffer.kt | 3 +- .../kotlin/sdk/shared/Transport.kt | 6 +- .../sdk/shared/WebSocketMcpTransport.kt | 1 - .../kotlin/sdk/types/McpException.kt | 10 +- .../kotlin/sdk/types/PingRequest.kt | 3 +- .../kotlin/sdk/types/common.kt | 9 +- .../kotlin/sdk/types/completion.kt | 4 +- .../kotlin/sdk/types/content.kt | 6 +- .../kotlin/sdk/types/elicitation.kt | 15 +- .../kotlin/sdk/types/jsonRpc.kt | 11 +- .../kotlin/sdk/types/methods.kt | 3 +- .../kotlin/sdk/types/notification.kt | 12 +- .../kotlin/sdk/types/request.kt | 13 +- .../kotlin/sdk/types/resources.kt | 12 +- .../kotlin/sdk/types/roots.kt | 2 +- .../kotlin/sdk/types/schema.kt | 20 +- .../kotlin/sdk/types/serializers.kt | 4 - .../kotlin/sdk/types/tasks.kt | 23 +- .../kotlin/sdk/types/tools.kt | 2 +- .../sdk/utils/PathSegmentTemplateMatcher.kt | 9 +- .../sdk/utils/ResourceTemplateMatcher.kt | 3 +- .../kotlin/sdk/types/ElicitationTest.kt | 3 - .../kotlin/sdk/types/InitializeTest.kt | 1 - .../kotlin/sdk/types/PrimitiveSchemaTest.kt | 2 - .../kotlin/sdk/types/SamplingTest.kt | 1 - .../kotlin/sdk/types/ToolsTest.kt | 1 - .../kotlin/sdk/types/dsl/SamplingDslTest.kt | 1 - kotlin-sdk-server/build.gradle.kts | 1 + .../detekt-baseline-commonMainSourceSet.xml | 13 - kotlin-sdk-server/detekt-baseline-main.xml | 23 - .../kotlin/sdk/server/ClientConnection.kt | 2 - .../kotlin/sdk/server/Feature.kt | 1 + .../sdk/server/FeatureNotificationService.kt | 3 +- .../kotlin/sdk/server/KtorServer.kt | 4 - .../kotlin/sdk/server/SSEServerTransport.kt | 4 +- .../kotlin/sdk/server/Server.kt | 14 +- .../kotlin/sdk/server/ServerSession.kt | 25 +- .../kotlin/sdk/server/StdioServerTransport.kt | 4 - .../server/StreamableHttpServerTransport.kt | 14 +- .../WebSocketMcpKtorServerExtensions.kt | 2 - .../sdk/server/AbstractKtorExtensionsTest.kt | 1 - .../sdk/server/StdioServerTransportTest.kt | 3 - kotlin-sdk-testing/build.gradle.kts | 1 + .../kotlin/sdk/testing/ChannelTransport.kt | 20 +- .../sdk/testing/ChannelTransportTest.kt | 1 - .../kotlin/sdk/package.kt | 26 +- test-utils/build.gradle.kts | 1 + test-utils/detekt-baseline-main.xml | 20 + test-utils/detekt-baseline.xml | 8 - .../kotlin/test/utils/Retry.kt | 1 - .../kotlin/test/utils/TypeScriptRunner.kt | 2 - .../kotlin/test/utils/processUtils.kt | 1 - 80 files changed, 1113 insertions(+), 514 deletions(-) create mode 100644 buildSrc/src/main/kotlin/mcp.detekt.gradle.kts delete mode 100644 integration-test/detekt-baseline-test.xml delete mode 100644 kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml delete mode 100644 kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml delete mode 100644 kotlin-sdk-client/detekt-baseline-test.xml delete mode 100644 kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml delete mode 100644 kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml create mode 100644 test-utils/detekt-baseline-main.xml delete mode 100644 test-utils/detekt-baseline.xml diff --git a/.claude/skills/kdoc/SKILL.md b/.claude/skills/kdoc/SKILL.md index 4760293..cfacfc3 100644 --- a/.claude/skills/kdoc/SKILL.md +++ b/.claude/skills/kdoc/SKILL.md @@ -5,40 +5,62 @@ description: "Add KDoc documentation to Kotlin public API. Use whenever the user # KDoc Generator -Add KDoc comments to public Kotlin API declarations. +Add KDoc to all public Kotlin declarations (classes, interfaces, objects, functions, properties, enums, type aliases, annotations — including `@Deprecated`). Read the implementation, not just the signature, to write accurate descriptions. -## What to document +## Tag placement -All public declarations (no explicit `private`/`internal`/`protected`): -- Classes, interfaces, objects, sealed classes/interfaces -- Functions, extension functions -- Properties -- Enum classes and entries -- Type aliases, annotation classes +| Where the declaration lives | Tag | +|---|---| +| Primary-ctor `val` / `var` (public, internal, or protected) | `@property` in class KDoc | +| Primary-ctor `name: Type` (no `val`/`var`) | `@param` in class KDoc | +| Primary-ctor `private val` / `private var` | `@param` in class KDoc | +| Body of class/object/interface/sealed interface | inline `/** */` above the declaration | +| Function parameter | `@param` | +| Non-Unit return type | `@return` | -Include `@Deprecated` elements. +## Style -## Context +- Summary: one concise sentence, third-person verb ("Creates", "Returns", "Represents"). Expand to 2-3 sentences only when genuinely complex. +- **Example block**: mandatory for DSL builders (show full receiver-scope usage); add for complex functions or non-obvious extension functions; skip for one-liners and simple getters. +- KDoc links (`[ClassName]`, `[methodName]`) only where they add navigational value. +- Don't write `@throws` or "suspend" notes. +- Rewrite existing KDoc if incomplete or low quality. -Read the implementation, not just the signature, to write accurate descriptions. Understanding what the code actually does prevents superficial or misleading documentation. +## Examples -## KDoc format - -**Class/interface example:** +**Class with mixed primary-constructor parameters:** ````kotlin /** * Manages active client sessions and their lifecycle. * - * Sessions are created on first connection and cleaned up - * when the transport closes. - * * @property maxSessions upper limit on concurrent sessions - * @property timeout idle timeout before a session is evicted + * @param scope coroutine scope for session lifecycle tasks + * @param secret used internally for session token signing */ +public class SessionManager( + public val maxSessions: Int, // public val → @property + scope: CoroutineScope, // no val/var → @param + private val secret: ByteArray, // private val → @param +) ```` -**Function example:** +**Body and interface properties (inline `/** */`):** + +````kotlin +/** Represents an entity that includes additional metadata. */ +public interface WithMeta { + /** Optional metadata attached to this entity. */ + public val meta: JsonObject? +} + +public class Connection(public val id: String) { + /** Capabilities negotiated during the handshake, or `null` before initialize completes. */ + public val capabilities: ServerCapabilities? get() = ... +} +```` + +**Function with Example block:** ````kotlin /** @@ -46,10 +68,7 @@ Read the implementation, not just the signature, to write accurate descriptions. * * Example: * ```kotlin - * server.addTool( - * name = "echo", - * description = "Echoes input back", - * ) { request -> + * server.addTool(name = "echo", description = "Echoes input back") { request -> * CallToolResult(content = listOf(TextContent("Echo: ${request.arguments}"))) * } * ``` @@ -60,15 +79,3 @@ Read the implementation, not just the signature, to write accurate descriptions. * @return the registered tool definition */ ```` - -## Rules - -- Summary: concise first sentence starting with a third-person verb ("Creates", "Returns", "Represents"). Expand to 2-3 sentences only when genuinely complex -- **@property** in class-level KDoc for all public properties (never as individual KDoc on the property); **@param** for function parameters -- **@return** for non-Unit return types -- **Example block**: add for DSL builders, complex functions, extension functions with non-obvious usage. Skip for trivial one-liners and simple getters -- **DSL builders**: always include an Example showing full usage with the receiver scope — this is critical for discoverability -- KDoc links (`[ClassName]`, `[methodName]`): only where it adds clear navigational value -- No **@throws** — don't document exceptions -- No **suspend** notes — coroutine nature is visible from the signature -- **Existing KDoc**: rewrite if incomplete (missing @param/@return/@property) or low quality \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index caaa3f0..ad4cc7a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: include: - os: ubuntu-latest job-name: "Linux Tests" - gradle-tasks: "ktlintCheck build" + gradle-tasks: "build" max-workers: 3 - os: macos-latest diff --git a/build.gradle.kts b/build.gradle.kts index 72c5553..2c2dbe3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,5 @@ -import dev.detekt.gradle.extensions.FailOnSeverity - plugins { id("mcp.dokka") - alias(libs.plugins.detekt) alias(libs.plugins.ktlint) alias(libs.plugins.kover) } @@ -23,16 +20,6 @@ dependencies { subprojects { apply(plugin = "org.jlleitschuh.gradle.ktlint") apply(plugin = "org.jetbrains.kotlinx.kover") - - if (name != "conformance-test" && name != "docs") { - apply(plugin = "dev.detekt") - - detekt { - config = files("$rootDir/config/detekt/detekt.yml") - buildUponDefaultConfig = true - failOnSeverity.set(FailOnSeverity.Error) - } - } } dokka { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 84676a4..4a057ce 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,6 +8,7 @@ repositories { } dependencies { + implementation(libs.detekt.gradle) implementation(libs.kotlin.gradle) implementation(libs.kotlin.serialization) implementation(libs.kotlinx.atomicfu.gradle) diff --git a/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts b/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts new file mode 100644 index 0000000..3c1419e --- /dev/null +++ b/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts @@ -0,0 +1,23 @@ +import dev.detekt.gradle.Detekt +import dev.detekt.gradle.extensions.FailOnSeverity + +plugins { + id("dev.detekt") +} + +detekt { + config = files("$rootDir/config/detekt/detekt.yml") + buildUponDefaultConfig = false + parallel = true + failOnSeverity = FailOnSeverity.Error +} + +tasks.withType().configureEach { + exclude("**/generated/**", "**/generated-sources/**", "**/build/**") +} + +pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") { + tasks.named("detekt").configure { + dependsOn("detektMainJvm") + } +} diff --git a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts index 2903cff..98a0a5e 100644 --- a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts +++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts @@ -57,12 +57,3 @@ kotlin { explicitApi = ExplicitApiMode.Strict jvmToolchain(21) } - -tasks.named("detekt").configure { - dependsOn( - "detektMainJvm", - "detektCommonMainSourceSet", - "detektTestJvm", - "detektCommonTestSourceSet", - ) -} diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 4d44734..98fce0e 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -3,41 +3,840 @@ config: warningsAsErrors: false checkExhaustiveness: false -complexity: - LargeClass: - excludes: &testFolders - - '**/test/**' - - '**/commonTest/**' - - '**/jvmTest/**' - - '**/jsTest/**' - - '**/iosTest/**' - LongMethod: - excludes: *testFolders +processors: + active: false -coroutines: - InjectDispatcher: - excludes: *testFolders - -empty-blocks: - EmptyFunctionBlock: - excludes: *testFolders +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'IssuesReport' + - 'FileBasedIssuesReport' + # - 'LiteIssuesReport' comments: - UndocumentedPublicClass: - active: true - excludes: &testAndGeneratedFolders - - '**/test/**' - - '**/commonTest/**' - - '**/jvmTest/**' - - '**/jsTest/**' - - '**/iosTest/**' - - '**/generated-sources/**' - ignoreDefaultCompanionObject: true - UndocumentedPublicFunction: - active: true - excludes: *testAndGeneratedFolders - UndocumentedPublicProperty: - active: true - excludes: *testAndGeneratedFolders - searchProtectedProperty: false - ignoreEnumEntries: true + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateIsRegex: false + licenseTemplate: '' + DeprecatedBlockTag: + active: true + DocumentationOverPrivateFunction: + active: false + DocumentationOverPrivateProperty: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + ignoreDefaultCompanionObject: true + UndocumentedPublicFunction: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false + ignoreEnumEntries: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + allowedComplexity: 20 + ComplexCondition: + active: true + allowedConditions: 4 + ComplexInterface: + active: false + allowedDefinitions: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: false + allowedComplexity: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + ignoreLocalFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: false + allowedLines: 700 + LongMethod: + active: false + allowedLines: 110 + LongParameterList: + active: false + allowedFunctionParameters: 8 + allowedConstructorParameters: 8 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + allowedOverloads: 6 + NamedArguments: + active: false + allowedArguments: 3 + ignoreMethods: [] + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + allowedDepth: 4 + NestedScopeFunctions: + active: false + allowedDepth: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + allowedDuplications: 3 + ignoreAnnotation: true + allowedWithLengthLessThan: 5 + ignoreStringsRegex: '$^' + TooManyFunctions: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + allowedFunctionsPerFile: 20 + allowedFunctionsPerClass: 20 + allowedFunctionsPerInterface: 20 + allowedFunctionsPerObject: 20 + allowedFunctionsPerEnum: 11 + ignoreDeprecated: true + ignorePrivate: true + ignoreInternal: true + ignoreOverridden: false + ignoreAnnotatedFunctions: [] + +coroutines: + active: true + CoroutineLaunchedInTestWithoutRunTest: + active: false + GlobalCoroutineUsage: + active: true + InjectDispatcher: + active: false + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunInFinallySection: + active: false + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + aliases: ['SuspendFunctionOnCoroutineScope'] + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKotlinFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ErrorUsageWithThrowable: + active: false + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: true + ObjectExtendsThrowable: + active: true + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: false + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + aliases: ['ClassName'] + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + aliases: ['EnumEntryName'] + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionNameMaxLength: + active: false + aliases: ['FunctionMaxNameLength'] + maximumFunctionNameLength: 30 + FunctionNameMinLength: + active: false + aliases: ['FunctionMinNameLength'] + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + aliases: ['FunctionName'] + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + aliases: ['PackageDirectoryMismatch'] + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: false + mustBeFirst: true + multiplatformTargets: + - 'ios' + - 'android' + - 'js' + - 'jvm' + - 'native' + - 'iosArm64' + - 'iosX64' + - 'macosX64' + - 'mingwX64' + - 'linuxX64' + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: true + ObjectPropertyNaming: + active: true + aliases: ['ObjectPropertyName'] + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + aliases: ['PackageName'] + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: false + aliases: ['PropertyName'] + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + allowedOperations: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryInitOnArray: + active: false + UnnecessaryPartOfBinaryExpression: + active: true + UnnecessaryTemporaryInstantiation: + active: true + UnnecessaryTypeCasting: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: true + ignorePlatformTypes: true + CastToNullableType: + active: false + CharArrayToStringCall: + active: false + Deprecation: + active: false + aliases: ['DEPRECATION'] + excludeImportStatements: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + aliases: ['DoubleMutability'] + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.Function*' + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + ignoreAnnotated: ['Test'] + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: true + excludes: ['**/*.kts'] + MissingSuperCall: + active: false + mustInvokeSuperAnnotations: + - 'androidx.annotation.CallSuper' + - 'javax.annotation.OverridingMethodsMustInvokeSuper' + MissingUseCall: + active: false + ignoreClass: + - 'java.io.ByteArrayInputStream' + - 'java.io.ByteArrayOutputStream' + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnamedParameterUse: + active: false + allowAdjacentDifferentTypeParams: true + allowSingleParamUse: true + ignoreArgumentsMatchingNames: true + ignoreFunctionCall: [] + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + aliases: ['UNCHECKED_CAST'] + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AbstractClassCanBeConcreteClass: + active: true + AbstractClassCanBeInterface: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeExpression: + active: false + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaMultipleParameters: + active: true + ExplicitItLambdaParameter: + active: false + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: true + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + forbiddenImports: [] + allowedImports: [] + ForbiddenMethodCall: + active: true + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + - reason: 'using `BigDecimal(Double)` can result in unexpected floating point precision behavior. Use `BigDecimal.valueOf(Double)` or `String.toBigDecimalOrNull()` instead.' + value: 'java.math.BigDecimal.(kotlin.Double)' + - reason: 'using `BigDecimal(String)` can result in a `NumberFormatException`. Use `String.toBigDecimalOrNull()`' + value: 'java.math.BigDecimal.(kotlin.String)' + - reason: 'It is marked as obsolete. Use `kotlin.time.measureTime` instead.' + value: 'kotlin.system.measureTimeMillis' + ForbiddenNamedParam: + active: false + methods: [] + ForbiddenOptIn: + active: false + markerClasses: [] + ForbiddenSuppress: + active: false + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: true + ignoreLocalVariableDeclaration: true + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: false + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConstant: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + ProtectedMemberInFinalClass: + active: true + RangeUntilInsteadOfRangeTo: + active: true + RedundantConstructorKeyword: + active: false + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifier: + active: false + ReturnCount: + active: false + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingAfterPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: false + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAny: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryFullyQualifiedName: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UnnecessaryReversed: + active: false + UnusedImport: + active: false + additionalOperatorSet: [] + UnusedParameter: + active: true + aliases: ['UNUSED_PARAMETER', 'unused'] + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + aliases: ['unused'] + UnusedPrivateFunction: + active: true + aliases: ['unused'] + allowedNames: '' + UnusedPrivateProperty: + active: true + aliases: ['unused'] + allowedNames: 'ignored|expected|serialVersionUID' + UnusedVariable: + active: true + aliases: ['UNUSED_VARIABLE', 'unused'] + allowedNames: 'ignored|_' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + aliases: ['CanBeVal'] + ignoreLateinitVar: false + WildcardImport: + active: false + excludeImports: + - 'java.util.*' diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt index dc126f6..e288272 100644 --- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt +++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt @@ -45,7 +45,6 @@ internal const val WAV_BASE64 = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABA private val logger = KotlinLogging.logger {} -@Suppress("LongMethod") fun Server.registerConformanceTools() { // 1. Simple text addTool( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca67d7e..360a468 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # plugins version atomicfu = "0.32.1" binaryCompatibilityValidatorPlugin = "0.18.1" -detekt = "2.0.0-alpha.2" +detekt = "2.0.0-alpha.3" dokka = "2.2.0" kotlin = "2.3.21" kover = "0.9.8" @@ -28,6 +28,7 @@ junit = "5.14.3" [libraries] # Plugins +detekt-gradle = { module = "dev.detekt:detekt-gradle-plugin", version.ref = "detekt" } dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } @@ -75,7 +76,6 @@ ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = " ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" } [plugins] -detekt = { id = "dev.detekt", version.ref = "detekt" } kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidatorPlugin" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } diff --git a/integration-test/build.gradle.kts b/integration-test/build.gradle.kts index 30afe9e..7a3a4a3 100644 --- a/integration-test/build.gradle.kts +++ b/integration-test/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("mcp.multiplatform") + id("mcp.detekt") } kotlin { diff --git a/integration-test/detekt-baseline-test.xml b/integration-test/detekt-baseline-test.xml deleted file mode 100644 index 2df611a..0000000 --- a/integration-test/detekt-baseline-test.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - AbstractClassCanBeConcreteClass:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$AbstractPromptIntegrationTest - AbstractClassCanBeConcreteClass:AbstractResourceIntegrationTest.kt:AbstractResourceIntegrationTest$AbstractResourceIntegrationTest - AbstractClassCanBeConcreteClass:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$AbstractToolIntegrationTest - AbstractClassCanBeConcreteClass:BaseTransportTest.kt:BaseTransportTest$BaseTransportTest - CyclomaticComplexMethod:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$private fun setupCalculatorTool - ForbiddenComment:StdioClientTransportTest.kt:StdioClientTransportTest$// TODO: fix running on windows - MatchingDeclarationName:PromptIntegrationTestSse.kt:SchemaPromptIntegrationTestSse : AbstractPromptIntegrationTest - ThrowsCount:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$override fun configureServer - UseCheckOrError:ClientTest.kt:ClientTest.<no name provided>$throw IllegalStateException("Test error") - UseOrEmpty:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$(request.params.arguments?.get("tags") as? JsonArray)?.mapNotNull { (it as? JsonPrimitive)?.content } ?: emptyList() - UseRequire:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$throw IllegalArgumentException("Missing required argument: $argName") - - diff --git a/kotlin-sdk-client/build.gradle.kts b/kotlin-sdk-client/build.gradle.kts index a3645b7..ae7a314 100644 --- a/kotlin-sdk-client/build.gradle.kts +++ b/kotlin-sdk-client/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("mcp.multiplatform") + id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) diff --git a/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml deleted file mode 100644 index 5cddbe1..0000000 --- a/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - CyclomaticComplexMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize - LongMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize - MaxLineLength:StdioClientTransport.kt:StdioClientTransport$* - ThrowsCount:SseClientTransport.kt:SseClientTransport$private suspend fun CoroutineScope.collectMessages - ThrowsCount:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$@Suppress("ReturnCount", "CyclomaticComplexMethod") override suspend fun performSend - TooGenericExceptionCaught:Client.kt:Client$error: Throwable - TooGenericExceptionCaught:SseClientTransport.kt:SseClientTransport$e: Throwable - TooGenericExceptionCaught:StdioClientTransport.kt:StdioClientTransport$e: Throwable - - diff --git a/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml b/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml deleted file mode 100644 index cab4322..0000000 --- a/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""" - MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}""" - - diff --git a/kotlin-sdk-client/detekt-baseline-main.xml b/kotlin-sdk-client/detekt-baseline-main.xml index 8411ab0..c34e75c 100644 --- a/kotlin-sdk-client/detekt-baseline-main.xml +++ b/kotlin-sdk-client/detekt-baseline-main.xml @@ -2,18 +2,6 @@ - CyclomaticComplexMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize - InjectDispatcher:StdioClientTransport.kt:StdioClientTransport$Default - InjectDispatcher:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$Default - LongMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize - MaxLineLength:StdioClientTransport.kt:StdioClientTransport$* - NoNameShadowing:StdioClientTransport.kt:StdioClientTransport$source - ThrowsCount:SseClientTransport.kt:SseClientTransport$private suspend fun CoroutineScope.collectMessages - ThrowsCount:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$@Suppress("ReturnCount", "CyclomaticComplexMethod") override suspend fun performSend - TooGenericExceptionCaught:Client.kt:Client$error: Throwable - TooGenericExceptionCaught:SseClientTransport.kt:SseClientTransport$e: Throwable - TooGenericExceptionCaught:StdioClientTransport.kt:StdioClientTransport$e: Throwable - UseCheckOrError:Client.kt:Client$throw IllegalStateException( "Server does not support resource subscriptions (required for $method)", ) - UseCheckOrError:Client.kt:Client$throw IllegalStateException("Client does not support roots capability.") + LoopWithTooManyJumpStatements:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$while diff --git a/kotlin-sdk-client/detekt-baseline-test.xml b/kotlin-sdk-client/detekt-baseline-test.xml deleted file mode 100644 index dd6adfa..0000000 --- a/kotlin-sdk-client/detekt-baseline-test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - AbstractClassCanBeConcreteClass:AbstractStreamableHttpClientTest.kt:AbstractStreamableHttpClientTest$AbstractStreamableHttpClientTest - ForbiddenComment:StreamableHttpClientTest.kt:StreamableHttpClientTest$// TODO: how to get notifications via Client API? - LongParameterList:MockMcp.kt:MockMcp$fun handleJSONRPCRequest - LongParameterList:MockMcp.kt:MockMcp$fun handleWithResult - LongParameterList:MockMcp.kt:MockMcp$fun onInitialize - MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""" - MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}""" - UseOrEmpty:ClientMetaParameterTest.kt:ClientMetaParameterTest$exception.message ?: "" - - diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index 49b20d7..d71c091 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -83,7 +83,7 @@ private val logger = KotlinLogging.logger {} * Options for configuring the MCP client. * * @property capabilities The capabilities this client supports. - * @property enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with the server. + * @param enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with the server. */ public class ClientOptions( public val capabilities: ClientCapabilities = ClientCapabilities(), @@ -125,7 +125,6 @@ public suspend fun mcpClient( * @param clientInfo Information about the client implementation (name, version). * @param options Configuration options for this client. */ -@Suppress("TooManyFunctions") public open class Client(private val clientInfo: Implementation, options: ClientOptions = ClientOptions()) : Protocol(options) { @@ -242,7 +241,7 @@ public open class Client(private val clientInfo: Implementation, options: Client Method.Defined.CompletionComplete, -> { checkNotNull(serverCapabilities?.prompts) { - throw IllegalStateException("Server does not support prompts (required for $method)") + "Server does not support prompts (required for $method)" } } @@ -255,10 +254,10 @@ public open class Client(private val clientInfo: Implementation, options: Client val resCaps = serverCapabilities?.resources ?: error("Server does not support resources (required for $method)") - if (method == Method.Defined.ResourcesSubscribe && resCaps.subscribe != true) { - throw IllegalStateException( - "Server does not support resource subscriptions (required for $method)", - ) + if (method == Method.Defined.ResourcesSubscribe) { + check(resCaps.subscribe == true) { + "Server does not support resource subscriptions (required for $method)" + } } } @@ -556,9 +555,8 @@ public open class Client(private val clientInfo: Implementation, options: Client * @throws IllegalStateException If the client does not support roots. */ public fun removeRoot(uri: String): Boolean { - if (capabilities.roots == null) { - logger.error { "Failed to remove root '$uri': Client does not support roots capability" } - throw IllegalStateException("Client does not support roots capability.") + checkNotNull(capabilities.roots) { + "Client does not support roots capability." } logger.info { "Removing root: $uri" } val oldMap = roots.getAndUpdate { current -> current.remove(uri) } @@ -636,7 +634,6 @@ public open class Client(private val clientInfo: Implementation, options: Client // --- Internal Handlers --- - @Suppress("ReturnCount") private fun applyElicitationDefaults(request: ElicitRequest, result: ElicitResult): ElicitResult { if (result.action != ElicitResult.Action.Accept) return result val formParams = request.params as? ElicitRequestFormParams ?: return result diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt index a61ea4b..63f681c 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt @@ -227,7 +227,6 @@ public class StdioClientTransport @JvmOverloads public constructor( } override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { - @Suppress("SwallowedException") try { sendChannel.send(message) } catch (e: CancellationException) { @@ -290,7 +289,7 @@ public class StdioClientTransport @JvmOverloads public constructor( ) { val buffer = Buffer() try { - source.use { source -> + source.use { while (isActive) { val bytesRead = source.readAtMostTo(buffer, BUFFER_SIZE) if (bytesRead == -1L) { diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt index 999e25a..c27ed79 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt @@ -54,6 +54,7 @@ private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID" * Represents an error from the Streamable HTTP transport. * * @property code HTTP status code associated with the error, or `null` if unavailable + * @param message detailed error description appended to the exception message */ public class StreamableHttpError(public val code: Int? = null, message: String? = null) : Exception("Streamable HTTP error: $message") @@ -65,14 +66,17 @@ private sealed interface ConnectResult { } /** - * Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. - * It will connect to a server using HTTP POST for sending messages and HTTP GET with Server-Sent Events - * for receiving messages. + * Client transport implementing the MCP Streamable HTTP transport specification. * - * @property sessionId session identifier assigned by the server after initialization, or `null` before connection - * @property protocolVersion MCP protocol version negotiated with the server, or `null` before connection + * Sends messages via HTTP POST and receives messages via HTTP GET with Server-Sent Events. + * Supports automatic SSE reconnection with exponential backoff, stream resumption via the + * `Last-Event-ID` header, and explicit session termination. + * + * @param client Ktor HTTP client used for all requests + * @param url MCP endpoint URL + * @param reconnectionOptions reconnection backoff and retry-limit settings for the SSE stream + * @param requestBuilder builder applied to every outgoing HTTP request, e.g. for adding auth headers */ -@Suppress("TooManyFunctions") public class StreamableHttpClientTransport( private val client: HttpClient, private val url: String, @@ -98,8 +102,11 @@ public class StreamableHttpClientTransport( override val logger: KLogger = KotlinLogging.logger {} + /** Session identifier assigned by the server after initialization, or `null` before connection. */ public var sessionId: String? = null private set + + /** MCP protocol version negotiated with the server, or `null` before connection. */ public var protocolVersion: String? = null private var sseJob: Job? = null @@ -121,7 +128,6 @@ public class StreamableHttpClientTransport( /** * Sends a single message with optional resumption support */ - @Suppress("ReturnCount", "CyclomaticComplexMethod", "LongMethod", "TooGenericExceptionCaught", "ThrowsCount") override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) { logger.debug { "Client sending message via POST to $url: ${McpJson.encodeToString(message)}" } @@ -260,7 +266,6 @@ public class StreamableHttpClientTransport( var attempt = 0 var needsDelay = initialServerRetryDelay != null - @Suppress("LoopWithTooManyJumpStatements") while (isActive) { // Delay before (re)connection: skip only for first fresh SSE connection if (needsDelay) { @@ -296,7 +301,6 @@ public class StreamableHttpClientTransport( } } - @Suppress("TooGenericExceptionCaught") private suspend fun connectSse(lastEventId: String?): ConnectResult { logger.debug { "Client attempting to start SSE session at url: $url" } return try { @@ -363,7 +367,6 @@ public class StreamableHttpClientTransport( } } - @Suppress("TooGenericExceptionCaught") private suspend fun collectSse( session: ClientSSESession, replayMessageId: RequestId?, @@ -408,7 +411,6 @@ public class StreamableHttpClientTransport( return SseStreamResult(hasPrimingEvent, receivedResponse, localLastEventId, localServerRetryDelay) } - @Suppress("CyclomaticComplexMethod") private suspend fun handleInlineSse( response: HttpResponse, replayMessageId: RequestId?, @@ -455,7 +457,6 @@ public class StreamableHttpClientTransport( } } - @Suppress("LoopWithTooManyJumpStatements") while (!channel.isClosedForRead) { val line = channel.readUTF8Line() ?: break if (line.isEmpty()) { diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt index 67b2f07..8810552 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt @@ -834,7 +834,6 @@ class StreamableHttpClientTransportTest { transport.close() } - @Suppress("DEPRECATION") @Test fun testDeprecatedConstructorStillWorks() = runTest { val mockEngine = MockEngine { _ -> diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt index fb71904..de9d2b3 100644 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt @@ -39,7 +39,6 @@ internal class MockMcp(verbose: Boolean = false) : AutoCloseable { val url = "${mokksy.baseUrl()}/mcp" - @Suppress("LongParameterList") fun onInitialize( clientName: String? = null, sessionId: String, @@ -117,7 +116,6 @@ internal class MockMcp(verbose: Boolean = false) : AutoCloseable { } } - @Suppress("LongParameterList") fun handleWithResult( httpMethod: HttpMethod = HttpMethod.Post, jsonRpcMethod: String, @@ -153,7 +151,6 @@ internal class MockMcp(verbose: Boolean = false) : AutoCloseable { } } - @Suppress("LongParameterList") fun handleWithResult( httpMethod: HttpMethod = HttpMethod.Post, jsonRpcMethod: String, @@ -178,7 +175,6 @@ internal class MockMcp(verbose: Boolean = false) : AutoCloseable { ) } - @Suppress("LongParameterList") fun handleJSONRPCRequest( httpMethod: HttpMethod = HttpMethod.Post, jsonRpcMethod: String, diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt index a1fe89e..43be8c9 100644 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt @@ -118,16 +118,16 @@ internal class StreamableHttpClientTest : AbstractStreamableHttpClientTest() { ServerSentEvent( event = "message", id = "1", - data = @Suppress("MaxLineLength") //language=json + data = """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""", ), ) delay(200.milliseconds) emit( ServerSentEvent( - data = @Suppress("MaxLineLength") //language=json + data = """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""", ), ) diff --git a/kotlin-sdk-core/build.gradle.kts b/kotlin-sdk-core/build.gradle.kts index 967dae3..1d19f5d 100644 --- a/kotlin-sdk-core/build.gradle.kts +++ b/kotlin-sdk-core/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("mcp.multiplatform") + id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) diff --git a/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml deleted file mode 100644 index d774a5b..0000000 --- a/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - CyclomaticComplexMethod:jsonUtils.kt:@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) private fun convertToJsonElement: JsonElement - LongMethod:Protocol.kt:Protocol$public suspend fun <T : RequestResult> request: T - MagicNumber:completion.kt:CompleteResult.Completion$100 - MatchingDeclarationName:capabilities.dsl.kt:ClientCapabilitiesBuilder - MatchingDeclarationName:methods.kt:Method - MaxLineLength:AbstractClientTransport.kt:AbstractClientTransport$* - MaxLineLength:ClientTransportState.kt:ClientTransportState.Initializing$* - MaxLineLength:Protocol.kt:Protocol$"Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}" - MaxLineLength:Protocol.kt:Protocol$* - MaxLineLength:Protocol.kt:RequestOptions$* - MaxLineLength:Transport.kt:Transport$* - MaxLineLength:TransportSendOptions.kt:TransportSendOptions$* - ReturnCount:ReadBuffer.kt:ReadBuffer$public fun readMessage: JSONRPCMessage? - ThrowsCount:AbstractClientTransport.kt:AbstractClientTransport$public override suspend fun send - TooGenericExceptionCaught:AbstractClientTransport.kt:AbstractClientTransport$e: Exception - TooGenericExceptionCaught:Protocol.kt:Protocol$cause: Throwable - TooGenericExceptionCaught:Protocol.kt:Protocol$error: Throwable - TooGenericExceptionCaught:Protocol.kt:Protocol$sendError: Throwable - TooGenericExceptionCaught:ReadBuffer.kt:ReadBuffer$e: Exception - TooGenericExceptionCaught:WebSocketMcpTransport.kt:WebSocketMcpTransport$e: Exception - TooManyFunctions:Protocol.kt:Protocol - - diff --git a/kotlin-sdk-core/detekt-baseline-main.xml b/kotlin-sdk-core/detekt-baseline-main.xml index a22fbcd..d483d7e 100644 --- a/kotlin-sdk-core/detekt-baseline-main.xml +++ b/kotlin-sdk-core/detekt-baseline-main.xml @@ -2,31 +2,6 @@ - CyclomaticComplexMethod:jsonUtils.kt:@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) private fun convertToJsonElement: JsonElement - InjectDispatcher:utils.jvm.kt:IO - InstanceOfCheckForException:AbstractClientTransport.kt:AbstractClientTransport$e is McpException - LongMethod:Protocol.kt:Protocol$public suspend fun <T : RequestResult> request: T - MagicNumber:completion.kt:CompleteResult.Completion$100 - MatchingDeclarationName:capabilities.dsl.kt:ClientCapabilitiesBuilder - MatchingDeclarationName:methods.kt:Method - MaxLineLength:AbstractClientTransport.kt:AbstractClientTransport$* - MaxLineLength:ClientTransportState.kt:ClientTransportState.Initializing$* - MaxLineLength:Protocol.kt:Protocol$"Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}" - MaxLineLength:Protocol.kt:Protocol$* - MaxLineLength:Protocol.kt:RequestOptions$* - MaxLineLength:Transport.kt:Transport$* - MaxLineLength:TransportSendOptions.kt:TransportSendOptions$* - NoNameShadowing:Protocol.kt:Protocol$error - ReturnCount:ReadBuffer.kt:ReadBuffer$public fun readMessage: JSONRPCMessage? - ThrowsCount:AbstractClientTransport.kt:AbstractClientTransport$public override suspend fun send - TooGenericExceptionCaught:AbstractClientTransport.kt:AbstractClientTransport$e: Exception - TooGenericExceptionCaught:Protocol.kt:Protocol$cause: Throwable - TooGenericExceptionCaught:Protocol.kt:Protocol$error: Throwable - TooGenericExceptionCaught:Protocol.kt:Protocol$sendError: Throwable - TooGenericExceptionCaught:ReadBuffer.kt:ReadBuffer$e: Exception - TooGenericExceptionCaught:WebSocketMcpTransport.kt:WebSocketMcpTransport$e: Exception - TooManyFunctions:Protocol.kt:Protocol UnsafeCallOnNullableType:Protocol.kt:Protocol$response!! - UseCheckNotNull:Protocol.kt:Protocol$check(error != null) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt index 5f67999..27013d3 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt @@ -99,8 +99,10 @@ public abstract class AbstractClientTransport : AbstractTransport() { * proceeding with further operations. It is often used to enforce state invariants within the transport's * lifecycle mechanics, enabling robust error handling and debugging in case of unexpected state transitions. * - * @param expected The expected transport state. An exception will be thrown if the actual state does not match this value. - * @throws IllegalStateException If the current state does not match the expected state. + * @param expected The expected transport state. + * An exception will be thrown if the actual state does not match this value. + * @param lazyMessage Function that builds the exception message from the actual state when the check fails. + * Defaults to a message that compares the expected state against the observed one. */ protected fun checkState( expected: ClientTransportState, diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt index 7b88100..a738289 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt @@ -6,11 +6,13 @@ import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi /** - * Implements [onClose], [onError] and [onMessage] functions of [Transport] providing - * corresponding [_onClose], [_onError] and [_onMessage] properties to use for an implementation. + * Base class for [Transport] implementations that handles the bookkeeping for [onClose], [onError] + * and [onMessage] callback registration, exposing the composed callbacks to subclasses for invocation. + * + * The callbacks are composed: each call to [onClose], [onError], or [onMessage] chains the new block + * after any previously registered ones. */ @OptIn(ExperimentalAtomicApi::class) -@Suppress("PropertyName") public abstract class AbstractTransport : Transport { private val onCloseCalled = AtomicBoolean(false) private var _onClose: (() -> Unit) = {} diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 75cebd3..8f86c03 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -53,22 +53,16 @@ public typealias ProgressCallback = (Progress) -> Unit /** * Additional initialization options. * + * @property enforceStrictCapabilities whether to restrict emitted requests to only those that the + * remote side has indicated it can handle through its advertised capabilities. + * This does NOT affect checking of _local_ side capabilities, as it is considered a logic error + * to mis-specify those. + * Currently defaults to `false` for backwards compatibility with SDK versions that did not advertise + * capabilities correctly; in the future, this will default to `true`. * @property timeout default timeout for outgoing requests */ public open class ProtocolOptions( - /** - * Whether to restrict emitted requests to only those that the remote side has indicated - * that they can handle, through their advertised capabilities. - * - * Note that this DOES NOT affect checking of _local_ side capabilities, as it is - * considered a logic error to mis-specify those. - * - * Currently, this defaults to false, for backwards compatibility with SDK versions - * that did not advertise capabilities correctly. - * In the future, this will default to true. - */ public var enforceStrictCapabilities: Boolean = false, - public var timeout: Duration = DEFAULT_REQUEST_TIMEOUT, ) @@ -80,18 +74,18 @@ public val DEFAULT_REQUEST_TIMEOUT: Duration = 60.seconds /** * Options that can be given per request. * - * @property relatedRequestId if present, - * `relatedRequestId` is used to indicate to the transport which incoming request to associate this outgoing message with. - * @property resumptionToken the resumption token used to continue long-running requests that were interrupted. - * This allows clients to reconnect and continue from where they left off, if supported by the transport. - * @property onResumptionToken a callback that is invoked when the resumption token changes, if supported by the transport. - * This allows clients to persist the latest token for potential reconnection. + * @param relatedRequestId if present, used to indicate to the transport which incoming request to + * associate this outgoing message with. + * @param resumptionToken the resumption token used to continue long-running requests that were interrupted. + * Allows clients to reconnect and continue from where they left off, if supported by the transport. + * @param onResumptionToken callback invoked when the resumption token changes, if supported by the transport. + * Allows clients to persist the latest token for potential reconnection. * @property onProgress callback for progress notifications. - * If set, requests progress notifications from the remote end (if supported). - * When progress notifications are received, this callback will be invoked. + * If set, requests progress notifications from the remote end (if supported); + * when progress notifications are received, this callback is invoked. * @property timeout a timeout for this request. - * If exceeded, a McpException with code `RequestTimeout` will be raised from request(). - * If not specified, `DEFAULT_REQUEST_TIMEOUT` will be used as the timeout. + * If exceeded, a [McpException] with code `RequestTimeout` is raised from [Protocol.request]. + * If not specified, [DEFAULT_REQUEST_TIMEOUT] is used. */ public class RequestOptions( relatedRequestId: RequestId? = null, @@ -147,19 +141,18 @@ internal val COMPLETED = CompletableDeferred(Unit).also { it.complete(Unit) } * Implements MCP protocol framing on top of a pluggable transport, including * features like request/response linking, notifications, and progress. * - * @property transport the active transport, or `null` if not connected - * @property requestHandlers registered request handlers keyed by method name - * @property notificationHandlers registered notification handlers keyed by method name - * @property responseHandlers pending response handlers keyed by request ID - * @property progressHandlers registered progress callbacks keyed by progress token + * @property options protocol-level configuration; `null` falls back to defaults */ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptions?) { + /** The active transport, or `null` if not connected. */ public var transport: Transport? = null private set private val _requestHandlers: AtomicRef RequestResult?>> = atomic(persistentMapOf()) + + /** Registered request handlers keyed by method name. */ public val requestHandlers: Map< String, suspend ( @@ -171,17 +164,23 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio private val _notificationHandlers = atomic(persistentMapOf Unit>()) + + /** Registered notification handlers keyed by method name. */ public val notificationHandlers: Map Unit> get() = _notificationHandlers.value private val _responseHandlers: AtomicRef Unit>> = atomic(persistentMapOf()) + + /** Pending response handlers keyed by request ID. */ public val responseHandlers: Map Unit> get() = _responseHandlers.value private val _progressHandlers: AtomicRef> = atomic(persistentMapOf()) + + /** Registered progress callbacks keyed by progress token. */ public val progressHandlers: Map get() = _progressHandlers.value @@ -285,7 +284,6 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } } - @Suppress("ThrowsCount") private suspend fun onRequest(request: JSONRPCRequest) { logger.trace { "Received request: ${request.method} (id: ${request.id})" } @@ -312,7 +310,6 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio return } - @Suppress("TooGenericExceptionCaught", "InstanceOfCheckForException") try { val result = handler(request, RequestHandlerExtra()) logger.trace { "Request handled successfully: ${request.method} (id: ${request.id})" } @@ -395,7 +392,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio if (response != null) { handler(response, null) } else { - check(error != null) + checkNotNull(error) val mcpException = McpException( code = error.error.code, message = error.error.message, @@ -479,8 +476,8 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio try { @Suppress("UNCHECKED_CAST") result.complete(response!!.result as T) - } catch (error: Throwable) { - result.completeExceptionally(error) + } catch (e: Throwable) { + result.completeExceptionally(e) } } } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt index d4c8b43..c0c853f 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt @@ -39,7 +39,6 @@ public class ReadBuffer { val line = readNextLine() ?: return null if (line.isBlank()) continue - @Suppress("TooGenericExceptionCaught") val message = try { deserializeMessage(line) } catch (e: Exception) { @@ -78,7 +77,7 @@ public class ReadBuffer { if (braceIndex == -1) return null val trimmed = line.substring(braceIndex) - @Suppress("TooGenericExceptionCaught") + return try { deserializeMessage(trimmed) } catch (ignored: Exception) { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt index cb57547..a89c102 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt @@ -21,9 +21,9 @@ public interface Transport { /** * Sends a JSON-RPC message (request or response). * - * @property message The JSON-RPC message to send, either a request or a response. - * @property options Optional transport-specific options that control sending behavior. - * Different transport implementations may support different options. + * @param message the JSON-RPC message to send, either a request or a response + * @param options optional transport-specific options that control sending behavior; + * different transport implementations may support different options */ public suspend fun send(message: JSONRPCMessage, options: TransportSendOptions? = null) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt index 9fa1a0b..baf3723 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport.kt @@ -45,7 +45,6 @@ public abstract class WebSocketMcpTransport : AbstractTransport() { */ protected abstract suspend fun initializeSession() - @Suppress("ThrowsCount") override suspend fun start() { logger.debug { "Starting websocket transport" } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt index 6b12492..56c851b 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt @@ -6,11 +6,11 @@ import kotlin.jvm.JvmOverloads /** * Represents an error specific to the MCP protocol. * - * @property code The MCP/JSON‑RPC error code. - * @property data Optional additional error payload as a JSON element; `null` when not provided. - * @param message The error message. Used verbatim as [Exception.message] — no error code prefix is prepended. - * Defaults to `"MCP error $code"` when not provided. - * @param cause The original cause. + * @property code the MCP/JSON-RPC error code + * @param message the error message; used verbatim as [Exception.message] without an error-code prefix. + * Defaults to `"MCP error $code"` when not provided. + * @property data optional additional error payload as a JSON element + * @param cause the original cause */ public class McpException @JvmOverloads public constructor( public val code: Int, diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt index c45ed30..8280404 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable /** * Represents a ping request used to check if the connection is alive. * - * @property meta optional request metadata + * @property params optional request parameters carrying metadata */ @Serializable public data class PingRequest(override val params: BaseRequestParams? = null) : @@ -17,6 +17,7 @@ public data class PingRequest(override val params: BaseRequestParams? = null) : @EncodeDefault override val method: Method = Method.Defined.Ping + /** Convenience accessor for [params]'s metadata. */ public val meta: RequestMeta? get() = params?.meta } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt index ca08356..f65b1ed 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/common.kt @@ -28,11 +28,10 @@ public val SUPPORTED_PROTOCOL_VERSIONS: List = listOf( /** * Represents an entity that includes additional metadata in its responses. - * - * @property meta optional metadata attached to this entity */ @Serializable public sealed interface WithMeta { + /** Optional metadata attached to this entity. */ @SerialName("_meta") public val meta: JsonObject? } @@ -91,9 +90,6 @@ public data class Icon( ) { /** * The theme context for which an icon is designed. - * - * @property Light Icon designed for use with a light background (typically darker icon). - * @property Dark Icon designed for use with a dark background (typically lighter icon). */ @Serializable public enum class Theme { @@ -128,11 +124,10 @@ public enum class Role { * * References are used to point to other entities (prompts, resources, etc.) * without including their full definitions. - * - * @property type discriminator identifying the reference subtype */ @Serializable(with = ReferencePolymorphicSerializer::class) public sealed interface Reference { + /** Discriminator identifying the reference subtype. */ public val type: ReferenceType } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt index c782e3e..1eb5329 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/completion.kt @@ -12,7 +12,6 @@ import kotlinx.serialization.json.JsonObject * A request from the client to the server to ask for completion options. * * @property params The request parameters containing the argument to complete and its context. - * @property meta optional request metadata */ @Serializable public data class CompleteRequest(override val params: CompleteRequestParams) : ClientRequest { @@ -38,6 +37,7 @@ public data class CompleteRequest(override val params: CompleteRequestParams) : public val context: Context? get() = params.context + /** Convenience accessor for [params]'s metadata. */ public val meta: RequestMeta? get() = params.meta } @@ -48,6 +48,8 @@ public data class CompleteRequest(override val params: CompleteRequestParams) : * @property argument The argument's information for which completion options are requested. * @property ref A reference to either a prompt or resource template to complete within. * @property context Additional, optional context for generating completions. + * @property meta Optional metadata for this request. May include a progressToken for + * out-of-band progress notifications. */ @Serializable public data class CompleteRequestParams( diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt index d30c22e..815e5f6 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt @@ -39,11 +39,10 @@ public enum class ContentTypes(public val value: String) { /** * Base interface for all content blocks in the protocol. - * - * @property type discriminator identifying the content block subtype */ @Serializable(with = ContentBlockPolymorphicSerializer::class) public sealed interface ContentBlock : WithMeta { + /** Discriminator identifying the content block subtype. */ public val type: ContentTypes } @@ -63,11 +62,10 @@ public sealed interface MediaContent : * * Implemented by [TextContent], [ImageContent], [AudioContent], [ToolUseContent], * and [ToolResultContent]. - * - * @property type discriminator identifying the content block subtype */ @Serializable(with = SamplingMessageContentPolymorphicSerializer::class) public sealed interface SamplingMessageContent : WithMeta { + /** discriminator identifying the content block subtype */ public val type: ContentTypes } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt index bb303f0..a0ee74c 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt @@ -46,11 +46,10 @@ public data class ElicitRequest(override val params: ElicitRequestParams) : Serv * Represents the parameters for an `elicitation/create` request. * * Implementations: [ElicitRequestFormParams], [ElicitRequestURLParams]. - * - * @property message The message to present to the user describing what information is being requested. */ @Serializable(with = ElicitRequestParamsSerializer::class) public sealed interface ElicitRequestParams : RequestParams { + /** The message to present to the user describing what information is being requested. */ public val message: String /** @@ -64,7 +63,6 @@ public sealed interface ElicitRequestParams : RequestParams { * Each property must be a primitive type (string, number, boolean). * @property required Optional list of property names that must be provided by the user. * If omitted, all fields are considered optional. - * @property type Always "object" for elicitation schemas. */ @Serializable public data class RequestedSchema( @@ -73,6 +71,7 @@ public sealed interface ElicitRequestParams : RequestParams { val properties: Map, val required: List? = null, ) { + /** Always "object" for elicitation schemas. */ @EncodeDefault val type: String = "object" } @@ -91,7 +90,7 @@ public sealed interface ElicitRequestParams : RequestParams { ReplaceWith("ElicitRequestFormParams(message, requestedSchema = requestedSchema, meta = meta)"), DeprecationLevel.WARNING, ) -@Suppress("FunctionNaming", "FunctionName") +@Suppress("FunctionName") public fun ElicitRequestParams( message: String, requestedSchema: ElicitRequestParams.RequestedSchema, @@ -113,7 +112,6 @@ public fun ElicitRequestParams( * later via `tasks/result`. * @property requestedSchema A restricted subset of JSON Schema. Only top-level properties * are allowed, without nesting. - * @property mode The elicitation mode discriminator, always `"form"`. * @property meta Optional metadata. May include a progressToken for out-of-band progress notifications. */ @Serializable @@ -124,6 +122,7 @@ public data class ElicitRequestFormParams( @SerialName("_meta") override val meta: RequestMeta? = null, ) : ElicitRequestParams { + /** The elicitation mode discriminator, always `"form"`. */ @EncodeDefault public val mode: String = "form" } @@ -141,7 +140,6 @@ public data class ElicitRequestFormParams( * @property task If specified, the caller is requesting task-augmented execution. The request * will return a [CreateTaskResult] immediately, and the actual result can be retrieved * later via `tasks/result`. - * @property mode The elicitation mode discriminator, always `"url"`. * @property meta Optional metadata. May include a progressToken for out-of-band progress notifications. */ @Serializable @@ -153,6 +151,7 @@ public data class ElicitRequestURLParams( @SerialName("_meta") override val meta: RequestMeta? = null, ) : ElicitRequestParams { + /** The elicitation mode discriminator, always `"url"`. */ @EncodeDefault public val mode: String = "url" } @@ -182,10 +181,6 @@ public data class ElicitResult( /** * The user's response action to an elicitation request. - * - * @property Accept User submitted the form/confirmed the action. Content will be provided. - * @property Decline User explicitly declined the action. No content provided. - * @property Cancel User dismissed the dialog without making an explicit choice. No content provided. */ @Serializable public enum class Action { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt index 1a9ea2e..7eefdb5 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonRpc.kt @@ -104,11 +104,10 @@ internal fun JSONRPCNotification.fromJSON(): Notification = * Base interface for all JSON-RPC 2.0 messages. * * All messages in the MCP protocol follow the JSON-RPC 2.0 specification. - * - * @property jsonrpc the JSON-RPC protocol version, always `"2.0"` */ @Serializable(with = JSONRPCMessagePolymorphicSerializer::class) public sealed interface JSONRPCMessage { + /** The JSON-RPC protocol version, always `"2.0"`. */ public val jsonrpc: String } @@ -131,7 +130,6 @@ public data object JSONRPCEmptyMessage : JSONRPCMessage { * The server or client (depending on a direction) must respond with either a * [JSONRPCResponse] or [JSONRPCError] that has the same [id]. * - * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @property id A unique identifier for this request. The response will include the same ID. * Can be a string or number. UUID string is used by default. * @property method The name of the method to invoke (e.g., "tools/list", "resources/read"). @@ -144,6 +142,7 @@ public data class JSONRPCRequest @JvmOverloads public constructor( val method: String, val params: JsonElement? = null, ) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ @EncodeDefault override val jsonrpc: String = JSONRPC_VERSION @@ -186,13 +185,13 @@ public data class JSONRPCRequest @JvmOverloads public constructor( * * Examples: progress updates, resource change notifications, log messages. * - * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @property method The name of the notification method (e.g., "notifications/progress", * "notifications/resources/updated"). * @property params Optional parameters for the notification. Structure depends on the specific method. */ @Serializable public data class JSONRPCNotification(val method: String, val params: JsonElement? = null) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ @EncodeDefault override val jsonrpc: String = JSONRPC_VERSION } @@ -207,12 +206,12 @@ public data class JSONRPCNotification(val method: String, val params: JsonElemen * Sent in response to a [JSONRPCRequest] when the method execution succeeds. * The [id] must match the [id] of the original request. * - * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @property id The identifier from the original request. Used to match responses to requests. * @property result The result of the method execution. Structure depends on the method that was called. */ @Serializable public data class JSONRPCResponse(val id: RequestId, val result: RequestResult = EmptyResult()) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ @EncodeDefault override val jsonrpc: String = JSONRPC_VERSION } @@ -227,12 +226,12 @@ public data class JSONRPCResponse(val id: RequestId, val result: RequestResult = * Sent in response to a [JSONRPCRequest] when the method execution fails. * The [id] must match the [id] of the original request. * - * @property jsonrpc Always "2.0" to indicate JSON-RPC 2.0 protocol. * @property id The identifier from the original request. Used to match error responses to requests. * @property error Details about the error that occurred, including error code and message. */ @Serializable public data class JSONRPCError(val id: RequestId?, val error: RPCError) : JSONRPCMessage { + /** Always `"2.0"` to indicate JSON-RPC 2.0 protocol. */ @EncodeDefault override val jsonrpc: String = JSONRPC_VERSION } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt index dae2fc7..19f4da5 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt @@ -4,11 +4,10 @@ import kotlinx.serialization.Serializable /** * Represents a method in the protocol, which can be predefined or custom. - * - * @property value the string representation of this method name */ @Serializable(with = MethodSerializer::class) public sealed interface Method { + /** The string representation of this method name. */ public val value: String /** diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt index 24be040..3d33c21 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt @@ -11,13 +11,13 @@ import kotlinx.serialization.json.JsonObject /** * Represents a notification in the protocol. - * - * @property method the notification method identifier - * @property params optional notification parameters */ @Serializable(with = NotificationPolymorphicSerializer::class) public sealed interface Notification { + /** The notification method identifier. */ public val method: Method + + /** Optional notification parameters. */ public val params: NotificationParams? } @@ -34,9 +34,7 @@ public sealed interface ClientNotification : Notification public sealed interface ServerNotification : Notification /** - * Interface for notification parameter types. - * - * @property meta Optional metadata for the notification. + * Interface for notification parameter types. Inherits the optional `_meta` field from [WithMeta]. */ @Serializable public sealed interface NotificationParams : WithMeta @@ -76,13 +74,13 @@ public class Progress( * @property method The custom method name. By convention, custom methods often contain * organization-specific prefixes (e.g., "mycompany/custom_event"). * @property params Raw JSON parameters for the custom notification, if present. - * @property meta optional metadata for this notification */ @Serializable public data class CustomNotification(override val method: Method, override val params: BaseNotificationParams? = null) : ClientNotification, ServerNotification { + /** Convenience accessor for [params]'s metadata. */ public val meta: JsonObject? get() = params?.meta } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt index 9f52ec9..aacab9a 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt @@ -15,11 +15,11 @@ import kotlin.jvm.JvmInline * Metadata attached to a request's `_meta` field. * * @property json the raw JSON object containing the metadata - * @property progressToken optional progress token for tracking request progress */ @JvmInline @Serializable public value class RequestMeta(public val json: JsonObject) { + /** Optional progress token for tracking request progress. */ public val progressToken: ProgressToken? get() = json["progressToken"]?.let { element -> when { @@ -72,14 +72,14 @@ public data class BaseRequestParams(@SerialName("_meta") override val meta: Requ /** * Represents a request in the protocol. - * - * @property method the request method identifier - * @property params optional request parameters */ @OptIn(ExperimentalSerializationApi::class) @Serializable(with = RequestPolymorphicSerializer::class) public sealed interface Request { + /** The request method identifier. */ public val method: Method + + /** Optional request parameters. */ public val params: RequestParams? } @@ -142,7 +142,7 @@ public sealed interface ServerResult : RequestResult /** * An empty result for a request containing optional metadata. * - * @param meta Additional metadata for the response. Defaults to an empty JSON object. + * @property meta Additional metadata for the response. Defaults to an empty JSON object. */ @Serializable public data class EmptyResult(@SerialName("_meta") override val meta: JsonObject? = null) : @@ -151,11 +151,10 @@ public data class EmptyResult(@SerialName("_meta") override val meta: JsonObject /** * Represents a paginated result. - * - * @property nextCursor opaque token for retrieving the next page of results, or `null` if no more results */ @Serializable public sealed interface PaginatedResult : RequestResult { + /** Opaque token for retrieving the next page of results, or `null` if no more results. */ public val nextCursor: String? } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt index 86ce3ce..1afa0e6 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/resources.kt @@ -117,15 +117,14 @@ public data class ResourceTemplateReference(val uri: String) : Reference { } /** - * The contents of a specific resource or sub-resource. - * - * @property uri The URI of this resource. - * @property mimeType The MIME type of this resource, if known. - * @property meta Optional metadata for this response. + * The contents of a specific resource or sub-resource. Carries the optional `_meta` field via [WithMeta]. */ @Serializable(with = ResourceContentsPolymorphicSerializer::class) public sealed interface ResourceContents : WithMeta { + /** The URI of this resource. */ public val uri: String + + /** The MIME type of this resource, if known. */ public val mimeType: String? } @@ -136,6 +135,7 @@ public sealed interface ResourceContents : WithMeta { * This must only be set if the item can actually be represented as text (not binary data). * @property uri The URI of this resource. * @property mimeType The MIME type of this resource, if known. + * @property meta Optional metadata for these contents. */ @Serializable public data class TextResourceContents( @@ -152,6 +152,7 @@ public data class TextResourceContents( * @property blob A base64-encoded string representing the binary data of the item. * @property uri The URI of this resource. * @property mimeType The MIME type of this resource, if known. + * @property meta Optional metadata for these contents. */ @Serializable public data class BlobResourceContents( @@ -167,6 +168,7 @@ public data class BlobResourceContents( * * @property uri The URI of this resource. * @property mimeType The MIME type of this resource, if known. + * @property meta Optional metadata for these contents. */ @Serializable public data class UnknownResourceContents( diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt index 194ea4f..7ee4f9e 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.kt @@ -53,7 +53,6 @@ public data class Root( * `listChanged = true` to receive these requests. * * @property params Optional request parameters containing metadata. - * @property meta optional request metadata */ @Serializable public data class ListRootsRequest(override val params: BaseRequestParams? = null) : ServerRequest { @@ -61,6 +60,7 @@ public data class ListRootsRequest(override val params: BaseRequestParams? = nul @EncodeDefault override val method: Method = Method.Defined.RootsList + /** Convenience accessor for [params]'s metadata. */ public val meta: RequestMeta? get() = params?.meta } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt index 479cc66..0982500 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/schema.kt @@ -24,7 +24,6 @@ public sealed interface PrimitiveSchemaDefinition * @property minLength Minimum string length. * @property maxLength Maximum string length. * @property format Optional format constraint (e.g., email, URI, date). - * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Serializable @@ -36,6 +35,7 @@ public data class StringSchema( val format: StringSchemaFormat? = null, val default: String? = null, ) : PrimitiveSchemaDefinition { + /** JSON Schema type discriminator, always `"string"`. */ @EncodeDefault public val type: String = "string" } @@ -73,7 +73,6 @@ public sealed interface NumberSchemaDefinition : PrimitiveSchemaDefinition * @property description Optional description for the field. * @property minimum Minimum allowed value. * @property maximum Maximum allowed value. - * @property type JSON Schema type discriminator, always `"integer"`. * @property default Optional default value. */ @Serializable @@ -84,6 +83,7 @@ public data class IntegerSchema( val maximum: Int? = null, val default: Int? = null, ) : NumberSchemaDefinition { + /** JSON Schema type discriminator, always `"integer"`. */ @EncodeDefault val type: String = "integer" } @@ -95,7 +95,6 @@ public data class IntegerSchema( * @property description Optional description for the field. * @property minimum Minimum allowed value. * @property maximum Maximum allowed value. - * @property type JSON Schema type discriminator, always `"number"`. * @property default Optional default value. */ @Serializable @@ -106,6 +105,7 @@ public data class DoubleSchema( val maximum: Double? = null, val default: Double? = null, ) : NumberSchemaDefinition { + /** JSON Schema type discriminator, always `"number"`. */ @EncodeDefault val type: String = "number" } @@ -115,7 +115,6 @@ public data class DoubleSchema( * * @property title Optional display title for the field. * @property description Optional description for the field. - * @property type JSON Schema type discriminator, always `"boolean"`. * @property default Optional default value. */ @Serializable @@ -124,6 +123,7 @@ public data class BooleanSchema( val description: String? = null, val default: Boolean? = null, ) : PrimitiveSchemaDefinition { + /** JSON Schema type discriminator, always `"boolean"`. */ @EncodeDefault val type: String = "boolean" } @@ -161,7 +161,6 @@ public sealed interface SingleSelectEnumSchema : EnumSchemaDefinition * @property title Optional display title for the field. * @property description Optional description for the field. * @property enumValues Array of enum values to choose from. - * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Serializable @@ -172,6 +171,7 @@ public data class UntitledSingleSelectEnumSchema( val enumValues: List, val default: String? = null, ) : SingleSelectEnumSchema { + /** JSON Schema type discriminator, always `"string"`. */ @EncodeDefault val type: String = "string" } @@ -182,7 +182,6 @@ public data class UntitledSingleSelectEnumSchema( * @property title Optional display title for the field. * @property description Optional description for the field. * @property oneOf Array of enum options with values and display labels. - * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Serializable @@ -192,6 +191,7 @@ public data class TitledSingleSelectEnumSchema( val oneOf: List, val default: String? = null, ) : SingleSelectEnumSchema { + /** JSON Schema type discriminator, always `"string"`. */ @EncodeDefault val type: String = "string" } @@ -205,7 +205,6 @@ public data class TitledSingleSelectEnumSchema( * @property description Optional description for the field. * @property enumValues Array of enum values to choose from. * @property enumNames Display names for enum values. Non-standard according to JSON Schema 2020-12. - * @property type JSON Schema type discriminator, always `"string"`. * @property default Optional default value. */ @Deprecated("Use TitledSingleSelectEnumSchema instead") @@ -218,6 +217,7 @@ public data class LegacyTitledEnumSchema( val enumNames: List? = null, val default: String? = null, ) : EnumSchemaDefinition { + /** JSON Schema type discriminator, always `"string"`. */ @EncodeDefault val type: String = "string" } @@ -238,7 +238,6 @@ public sealed interface MultiSelectEnumSchema : EnumSchemaDefinition * @property minItems Minimum number of items to select. * @property maxItems Maximum number of items to select. * @property items Schema for the array items. - * @property type JSON Schema type discriminator, always `"array"`. * @property default Optional default value. */ @Serializable @@ -250,13 +249,13 @@ public data class UntitledMultiSelectEnumSchema( val items: Items, val default: List? = null, ) : MultiSelectEnumSchema { + /** JSON Schema type discriminator, always `"array"`. */ @EncodeDefault val type: String = "array" /** * Schema for the array items with plain enum values. * - * @property type JSON Schema type discriminator, always `"string"`. * @property enumValues Array of enum values to choose from. */ @Serializable @@ -264,6 +263,7 @@ public data class UntitledMultiSelectEnumSchema( @SerialName("enum") val enumValues: List, ) { + /** JSON Schema type discriminator, always `"string"`. */ @EncodeDefault val type: String = "string" } @@ -277,7 +277,6 @@ public data class UntitledMultiSelectEnumSchema( * @property minItems Minimum number of items to select. * @property maxItems Maximum number of items to select. * @property items Schema for array items with enum options and display labels. - * @property type JSON Schema type discriminator, always `"array"`. * @property default Optional default value. */ @Serializable @@ -289,6 +288,7 @@ public data class TitledMultiSelectEnumSchema( val items: Items, val default: List? = null, ) : MultiSelectEnumSchema { + /** JSON Schema type discriminator, always `"array"`. */ @EncodeDefault val type: String = "array" diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt index 4a1abc6..ab85e6d 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt @@ -424,7 +424,6 @@ private fun selectClientResultDeserializer(element: JsonElement): Deserializatio * Selects the appropriate deserializer for server results based on JSON content. * Returns null if the structure doesn't match any known server result type. */ -@Suppress("CyclomaticComplexMethod") private fun selectServerResultDeserializer(element: JsonElement): DeserializationStrategy? { val jsonObject = element.jsonObject return when { @@ -530,7 +529,6 @@ internal object RequestIdPolymorphicSerializer : JsonContentPolymorphicSerialize internal object ElicitRequestParamsSerializer : JsonContentPolymorphicSerializer( ElicitRequestParams::class, ) { - @Suppress("ThrowsCount") override fun selectDeserializer(element: JsonElement): DeserializationStrategy { val mode = when (val modeElement = element.jsonObject["mode"]) { null -> null @@ -559,7 +557,6 @@ internal object ElicitRequestParamsSerializer : JsonContentPolymorphicSerializer internal object PrimitiveSchemaDefinitionSerializer : JsonContentPolymorphicSerializer( PrimitiveSchemaDefinition::class, ) { - @Suppress("ThrowsCount") override fun selectDeserializer(element: JsonElement): DeserializationStrategy { val obj = element.jsonObject val typeElement = obj["type"] @@ -589,7 +586,6 @@ internal object PrimitiveSchemaDefinitionSerializer : JsonContentPolymorphicSeri } } - @Suppress("DEPRECATION") private fun selectStringTypeDeserializer( obj: Map, ): DeserializationStrategy = when { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt index 0f154e5..7363561 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt @@ -17,22 +17,27 @@ public const val RELATED_TASK_META_KEY: String = "io.modelcontextprotocol/relate /** * Common fields shared by all types representing the task state. - * - * @property taskId the task identifier - * @property status current task state - * @property statusMessage optional human-readable message describing the current task state - * @property createdAt ISO 8601 timestamp when the task was created - * @property lastUpdatedAt ISO 8601 timestamp when the task was last updated - * @property ttl actual retention duration from creation in milliseconds, or `null` for unlimited - * @property pollInterval suggested polling interval in milliseconds */ public sealed interface TaskFields { + /** The task identifier. */ public val taskId: String + + /** Current task state. */ public val status: TaskStatus + + /** Optional human-readable message describing the current task state. */ public val statusMessage: String? + + /** ISO 8601 timestamp when the task was created. */ public val createdAt: String + + /** ISO 8601 timestamp when the task was last updated. */ public val lastUpdatedAt: String + + /** Actual retention duration from creation in milliseconds, or `null` for unlimited. */ public val ttl: Long? + + /** Suggested polling interval in milliseconds. */ public val pollInterval: Long? } @@ -237,13 +242,13 @@ public data class GetTaskPayloadRequestParams( * For example, a tools/call task would return the [CallToolResult] structure. * * @property json the raw JSON object containing the result payload - * @property meta Optional metadata for this response. */ @JvmInline @Serializable public value class GetTaskPayloadResult(public val json: JsonObject) : ClientResult, ServerResult { + /** Optional metadata for this response, extracted from the `_meta` field of [json]. */ override val meta: JsonObject? get() = json["_meta"]?.takeIf { it is JsonObject } as? JsonObject diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt index a03e8ad..2ff890a 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tools.kt @@ -84,7 +84,6 @@ public data class Tool( * @property schema Optional URI identifying the JSON Schema dialect (e.g., * `https://json-schema.org/draft/2020-12/schema`). Serialized as `$schema`. * When absent, JSON Schema 2020-12 is assumed by default. - * @property type Always "object" for tool schemas. * @property properties Optional map of property names to their schema definitions. * @property required Optional list of property names that are required. * @property defs Optional schema definitions available to references in [properties]. Serialized as `$defs`. @@ -98,6 +97,7 @@ public data class ToolSchema( @SerialName("\$defs") val defs: JsonObject? = null, ) { + /** Always `"object"` for tool schemas. */ @OptIn(ExperimentalSerializationApi::class) @EncodeDefault val type: String = "object" diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt index c5619d5..aa2abb9 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/PathSegmentTemplateMatcher.kt @@ -66,11 +66,9 @@ private const val VARIABLE_MATCH_SCORE = 1 * no normalizer is available, so dot-segment traversal is **not** mitigated at this * layer — handlers on those targets must normalize paths themselves. * - * @param resourceTemplate The resource template to match against. Must follow RFC 6570 Level 1 syntax. - * @param maxUriLength Maximum allowed length for incoming URIs. Defaults to 2048. - * @param maxDepth Maximum allowed segment count for the template/uri. Defaults to 50. - * - * @property resourceTemplate The resource template against which resource URIs will be matched. + * @property resourceTemplate the resource template to match against; must follow RFC 6570 Level 1 syntax + * @param maxDepth maximum allowed segment count for the template/uri (defaults to 50) + * @param maxUriLength maximum allowed length for incoming URIs (defaults to 2048) */ public class PathSegmentTemplateMatcher @JvmOverloads constructor( override val resourceTemplate: ResourceTemplate, @@ -118,7 +116,6 @@ public class PathSegmentTemplateMatcher @JvmOverloads constructor( variableIndices = vars.toImmutableMap() } - @Suppress("ReturnCount") override fun match(resourceUri: String): MatchResult? { if (resourceUri.length > maxUriLength) { logger.debug { "URL is too long (max=$maxUriLength)" } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt index 867cc9f..d99bfda 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcher.kt @@ -32,11 +32,10 @@ public class MatchResult(public val variables: Map, public val s * candidate URIs against that template via [match]. The returned [MatchResult.score] * must reflect match specificity so that a selection algorithm can prefer the most * specific template when multiple templates match the same URI. - * - * @property resourceTemplate the template to match URIs against */ public interface ResourceTemplateMatcher { + /** The template to match URIs against. */ public val resourceTemplate: ResourceTemplate /** diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt index 5392890..1a41863 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt @@ -176,7 +176,6 @@ class ElicitationTest { // ── Deprecated compat ─────────────────────────────────────────────── @Test - @Suppress("DEPRECATION") fun `deprecated factory function should create ElicitRequestFormParams`() { val schema = ElicitRequestParams.RequestedSchema( properties = mapOf("name" to StringSchema()), @@ -188,7 +187,6 @@ class ElicitationTest { } @Test - @Suppress("DEPRECATION") fun `deprecated requestedSchema should return schema for form mode`() { val schema = ElicitRequestParams.RequestedSchema( properties = mapOf("x" to BooleanSchema()), @@ -198,7 +196,6 @@ class ElicitationTest { } @Test - @Suppress("DEPRECATION") fun `deprecated requestedSchema should return null for URL mode`() { val request = ElicitRequest( ElicitRequestURLParams(message = "m", elicitationId = "id", url = "https://example.com"), diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt index ed97c18..e5797b9 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt @@ -15,7 +15,6 @@ import kotlin.test.assertNull class InitializeTest { @Test - @Suppress("LongMethod") fun `should serialize InitializeRequest with capabilities and meta`() { val request = InitializeRequest( InitializeRequestParams( diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt index 954fb1e..904454f 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PrimitiveSchemaTest.kt @@ -397,7 +397,6 @@ class PrimitiveSchemaTest { // ── LegacyTitledEnumSchema ──────────────────────────────────────────── - @Suppress("DEPRECATION") @Test fun `should serialize LegacyTitledEnumSchema`() { verifySerialization( @@ -420,7 +419,6 @@ class PrimitiveSchemaTest { ) } - @Suppress("DEPRECATION") @Test fun `should deserialize LegacyTitledEnumSchema from JSON`() { val json = """ diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt index 0432aba..62a82cd 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt @@ -106,7 +106,6 @@ class SamplingTest { } @Test - @Suppress("LongMethod") fun `should serialize CreateMessageRequest with all fields`() { val request = CreateMessageRequest( CreateMessageRequestParams( diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt index 87f1644..d5e1d5a 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt @@ -37,7 +37,6 @@ class ToolsTest { } @Test - @Suppress("LongMethod") fun `should serialize Tool with annotations and schemas`() { val inputSchema = ToolSchema( properties = buildJsonObject { diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt index 10be8b0..98817c1 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt @@ -28,7 +28,6 @@ import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) class SamplingDslTest { @Test - @Suppress("LongMethod") fun `buildCreateMessageRequest should build with all fields`() { val request = buildCreateMessageRequest { maxTokens = 1000 diff --git a/kotlin-sdk-server/build.gradle.kts b/kotlin-sdk-server/build.gradle.kts index 126c516..193eb90 100644 --- a/kotlin-sdk-server/build.gradle.kts +++ b/kotlin-sdk-server/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("mcp.multiplatform") + id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) diff --git a/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml deleted file mode 100644 index d383ec9..0000000 --- a/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - MagicNumber:StdioServerTransport.kt:StdioServerTransport$8192 - MaxLineLength:SSEServerTransport.kt:SseServerTransport$"SSEServerTransport already started! If using Server class, note that connect() calls start() automatically." - MaxLineLength:SSEServerTransport.kt:SseServerTransport$* - ReturnCount:KtorServer.kt:private suspend fun existingStreamableTransport: StreamableHttpServerTransport? - TooGenericExceptionCaught:SSEServerTransport.kt:SseServerTransport$e: Exception - TooGenericExceptionCaught:StdioServerTransport.kt:StdioServerTransport$e: Throwable - TooManyFunctions:KtorServer.kt:io.modelcontextprotocol.kotlin.sdk.server.KtorServer.kt - - diff --git a/kotlin-sdk-server/detekt-baseline-main.xml b/kotlin-sdk-server/detekt-baseline-main.xml index a27f38a..aad11fa 100644 --- a/kotlin-sdk-server/detekt-baseline-main.xml +++ b/kotlin-sdk-server/detekt-baseline-main.xml @@ -2,29 +2,6 @@ - InjectDispatcher:FeatureNotificationService.kt:FeatureNotificationService$Default - LongParameterList:KtorServer.kt:private suspend fun RoutingContext.streamableTransport: StreamableHttpServerTransport? - LongParameterList:StreamableHttpServerTransport.kt:StreamableHttpServerTransport.Configuration - MagicNumber:StdioServerTransport.kt:StdioServerTransport$8192 - MaxLineLength:SSEServerTransport.kt:SseServerTransport$"SSEServerTransport already started! If using Server class, note that connect() calls start() automatically." - MaxLineLength:SSEServerTransport.kt:SseServerTransport$* - NoNameShadowing:FeatureNotificationService.kt:FeatureNotificationService${ it.remove(job) } - ReturnCount:KtorServer.kt:private suspend fun existingStreamableTransport: StreamableHttpServerTransport? - TooGenericExceptionCaught:SSEServerTransport.kt:SseServerTransport$e: Exception - TooGenericExceptionCaught:StdioServerTransport.kt:StdioServerTransport$e: Throwable - TooManyFunctions:KtorServer.kt:io.modelcontextprotocol.kotlin.sdk.server.KtorServer.kt UnsafeCallOnNullableType:StreamableHttpServerTransport.kt:StreamableHttpServerTransport$responseRequestId!! - UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to add prompt '${prompt.name}': Server does not support prompts capability" } "Server does not support prompts capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to add prompts: Server does not support prompts capability" } "Server does not support prompts capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to remove prompt '$name': Server does not support prompts capability" } "Server does not support prompts capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to remove prompts: Server does not support prompts capability" } "Server does not support prompts capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to add resource '$name': Server does not support resources capability" } "Server does not support resources capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to add resources: Server does not support resources capability" } "Server does not support resources capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to remove resource '$uri': Server does not support resources capability" } "Server does not support resources capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.resources != null) { logger.error { "Failed to remove resources: Server does not support resources capability" } "Server does not support resources capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" } "Server does not support tools capability. Enable it in ServerOptions." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to add tools: Server does not support tools capability" } "Server does not support tools capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to remove tool '$name': Server does not support tools capability" } "Server does not support tools capability." } - UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to remove tools: Server does not support tools capability" } "Server does not support tools capability." } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt index e64b0da..9b4b2f3 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt @@ -33,7 +33,6 @@ private val logger = KotlinLogging.logger {} * communication through notifications, requests, and other operations. * This interface defines various methods to facilitate the interaction. */ -@Suppress("TooManyFunctions") public interface ClientConnection { /** @@ -176,7 +175,6 @@ public interface ClientConnection { public suspend fun sendElicitationComplete(notification: ElicitationCompleteNotification) } -@Suppress("TooManyFunctions") internal class ClientConnectionImpl(private val session: ServerSession) : ClientConnection { override val sessionId: String get() = session.sessionId diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt index ce55f3e..a892c6c 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt @@ -64,6 +64,7 @@ public data class RegisteredResource( * A registered resource template with its associated read handler. * * @property resourceTemplate The [ResourceTemplate] definition (RFC 6570 URI template). + * @property matcher Pre-built matcher used to test incoming URIs against [resourceTemplate]. * @property readHandler A suspend function invoked when a client reads a URI that matches * this template. The second parameter contains the URI variables extracted from the match. */ diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureNotificationService.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureNotificationService.kt index d996b2d..42c2b8f 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureNotificationService.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureNotificationService.kt @@ -38,6 +38,7 @@ private sealed class NotificationEvent(open val timestamp: Long) /** * Represents an event for a notification. * + * @property timestamp Timestamp at which this event was created. * @property notification The notification associated with the event. */ private class SendEvent(override val timestamp: Long, val notification: Notification) : NotificationEvent(timestamp) @@ -340,7 +341,7 @@ internal class FeatureNotificationService( emitJobs.getAndUpdate { it.add(job) } // Register completion - job.invokeOnCompletion { + job.invokeOnCompletion { _ -> emitJobs.getAndUpdate { it.remove(job) } } diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt index 806a479..3f03a7a 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt @@ -1,5 +1,3 @@ -@file:Suppress("TooManyFunctions") - package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging @@ -172,7 +170,6 @@ private fun Application.mcpStreamableHttp( * that creates and returns the [Server] to handle the connection. */ @KtorDsl -@Suppress("LongParameterList") public fun Application.mcpStreamableHttp( path: String = "/mcp", enableDnsRebindingProtection: Boolean = false, @@ -247,7 +244,6 @@ private fun Application.mcpStatelessStreamableHttp( * that creates and returns the [Server] to handle the connection. */ @KtorDsl -@Suppress("LongParameterList") public fun Application.mcpStatelessStreamableHttp( path: String = "/mcp", enableDnsRebindingProtection: Boolean = false, diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt index fcf1a46..a6d4618 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SSEServerTransport.kt @@ -27,13 +27,15 @@ internal const val SESSION_ID_PARAM = "sessionId" * * Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`. * - * @property sessionId unique identifier for this transport session, generated randomly on creation + * @param endpoint relative or absolute URL the client will POST messages to + * @param session active SSE session used to deliver server-to-client events */ @OptIn(ExperimentalAtomicApi::class) public class SseServerTransport(private val endpoint: String, private val session: ServerSSESession) : AbstractTransport() { private val initialized: AtomicBoolean = AtomicBoolean(false) + /** Unique identifier for this transport session, generated randomly on creation. */ @OptIn(ExperimentalUuidApi::class) public val sessionId: String = Uuid.random().toString() diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index d9b9b9a..1b7bfe7 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -64,7 +64,8 @@ private val logger = KotlinLogging.logger {} * Configuration options for the MCP server. * * @property capabilities The capabilities this server supports. - * @property enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with clients. + * @param enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with clients. + * Forwarded to [ProtocolOptions.enforceStrictCapabilities]. * @property resourceTemplateMatcherFactory The factory used to create [ResourceTemplateMatcher] instances * for matching resource URIs against registered templates. Defaults to [PathSegmentTemplateMatcher.factory]. */ @@ -92,13 +93,12 @@ public class ServerOptions( * Currently, after subscription to a resource, the server will NOT send the subscription confirmation * as this response schema is not defined in the protocol. * - * @param serverInfo Information about this server implementation (name, version). - * @param options Configuration options for the server. - * @param instructionsProvider Optional provider for instructions from the server to the client about how to use + * @property serverInfo Information about this server implementation (name, version). + * @property options Configuration options for the server. + * @property instructionsProvider Optional provider for instructions from the server to the client about how to use * this server. The provider is called each time a new session is started to support dynamic instructions. * @param block A block to configure the mcp server. */ -@Suppress("TooManyFunctions") public open class Server( protected val serverInfo: Implementation, protected val options: ServerOptions, @@ -312,9 +312,9 @@ public open class Server( * Registers a single tool. The client can then call this tool. * * @param name The name of the tool. - * @param title An optional human-readable name of the tool for display purposes. * @param description A human-readable description of what the tool does. * @param inputSchema The expected input schema for the tool. + * @param title An optional human-readable name of the tool for display purposes. * @param outputSchema The optional expected output schema for the tool. * @param toolAnnotations Optional additional tool information. * @param execution Optional execution-related properties, such as task-augmented execution support. @@ -322,7 +322,6 @@ public open class Server( * @param handler A suspend function that handles executing the tool when called by the client. * @throws IllegalStateException If the server does not support tools. */ - @Suppress("LongParameterList") public fun addTool( name: String, description: String, @@ -645,7 +644,6 @@ public open class Server( } // Execute the tool handler and catch any errors - @Suppress("TooGenericExceptionCaught") return try { logger.trace { "Executing tool ${requestParams.name} with input: ${requestParams.arguments}" } tool.run { diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt index 2f55c76..e7da26f 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -39,17 +39,28 @@ import kotlin.uuid.Uuid private val logger = KotlinLogging.logger {} /** - * Represents a server session. + * Represents an active server-side session in the Model Context Protocol. * - * @property sessionId unique identifier for this session, generated randomly on creation + * Owns the server side of a single client connection on top of [Protocol]. Drives the + * initialize/initialized handshake, tracks the negotiated client capabilities and version, + * asserts capability requirements before dispatching requests and notifications, and exposes + * server-to-client operations such as sampling, root listing, elicitation, logging, and + * list-change notifications. + * + * Equality and hashing are based on [sessionId], so a session can be used as a key in + * routing structures. + * + * @property serverInfo server name and version reported to the client during initialization + * @param options server capabilities and protocol options used by [Protocol] + * @property instructions optional human-readable instructions for the client, sent in the initialize result */ -@Suppress("TooManyFunctions") public open class ServerSession( protected val serverInfo: Implementation, options: ServerOptions, protected val instructions: String?, ) : Protocol(options) { + /** Unique identifier for this session, generated on creation. */ @OptIn(ExperimentalUuidApi::class) public val sessionId: String = Uuid.random().toString() @@ -60,14 +71,10 @@ public open class ServerSession( private val _clientCapabilities: AtomicRef = atomic(null) private val _clientVersion: AtomicRef = atomic(null) - /** - * The client's reported capabilities after initialization. - */ + /** Capabilities reported by the client during initialization, or `null` before the handshake completes. */ public val clientCapabilities: ClientCapabilities? get() = _clientCapabilities.value - /** - * The client's version information after initialization. - */ + /** Client implementation information reported during initialization, or `null` before the handshake completes. */ public val clientVersion: Implementation? get() = _clientVersion.value /** diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt index 8fd46dd..a4d588c 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport.kt @@ -72,7 +72,6 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: private fun launchReadingJob(): Job { val job = scope.launch { val buf = Buffer() - @Suppress("TooGenericExceptionCaught") try { while (isActive) { val bytesRead = inputStream.readAtMostTo(buf, READ_BUFFER_SIZE) @@ -103,7 +102,6 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: private fun launchProcessingJob(): Job { val job = scope.launch { - @Suppress("TooGenericExceptionCaught") try { for (chunk in readChannel) { readBuffer.append(chunk) @@ -123,7 +121,6 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: private fun launchSendingJob(): Job { val job = scope.launch { - @Suppress("TooGenericExceptionCaught") try { for (message in writeChannel) { val json = serializeMessage(message) @@ -147,7 +144,6 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: } private suspend fun processReadBuffer() { - @Suppress("TooGenericExceptionCaught") while (true) { val message = try { readBuffer.readMessage() diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index f232001..870a92c 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -74,10 +74,8 @@ private data class SessionContext(val session: ServerSSESession?, val call: Appl * - No session validation is performed * * @param configuration Transport configuration. See [Configuration] for available options. - * @property sessionId session identifier assigned after initialization, or `null` in stateless mode */ @OptIn(ExperimentalUuidApi::class, ExperimentalAtomicApi::class) -@Suppress("TooManyFunctions") public class StreamableHttpServerTransport(private val configuration: Configuration) : AbstractTransport() { @Deprecated("Use default constructor with explicit Configuration()") @@ -100,7 +98,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat * @param retryIntervalMillis Retry interval in milliseconds for event handling or reconnection attempts. * Defaults to `null`. */ - @Suppress("MaxLineLength") @Deprecated( "Use constructor with Configuration: StreamableHttpServerTransport(Configuration(enableJsonResponse = ...))", replaceWith = ReplaceWith( @@ -153,6 +150,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } + /** Session identifier assigned after initialization in stateful mode, or `null` in stateless mode. */ public var sessionId: String? = null private set @@ -215,7 +213,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("CyclomaticComplexMethod", "ReturnCount") override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { val responseRequestId: RequestId? = when (message) { is JSONRPCResponse -> message.id @@ -329,7 +326,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat /** * Handles POST requests containing JSON-RPC messages */ - @Suppress("CyclomaticComplexMethod", "LongMethod", "ReturnCount", "TooGenericExceptionCaught") public suspend fun handlePostRequest(session: ServerSSESession?, call: ApplicationCall) { try { if (!configuration.enableJsonResponse && session == null) { @@ -438,7 +434,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } /** Handles an HTTP GET request by establishing a standalone SSE stream for server-initiated notifications. */ - @Suppress("ReturnCount") public suspend fun handleGetRequest(session: ServerSSESession?, call: ApplicationCall) { // NOTE: enableJsonResponse only controls how POST responses are delivered (JSON vs. SSE). // The standalone GET SSE stream is always supported — it is the only channel available @@ -512,7 +507,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat * Closes the SSE stream associated with the given [requestId], prompting the client to reconnect. * Useful for implementing polling behavior for long-running operations. */ - @Suppress("ReturnCount", "TooGenericExceptionCaught") public suspend fun closeSseStream(requestId: RequestId) { if (configuration.enableJsonResponse) return val streamId = requestToStreamMapping[requestId] ?: return @@ -529,7 +523,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("TooGenericExceptionCaught") private suspend fun replayEvents(store: EventStore, lastEventId: String, session: ServerSSESession) { val call: ApplicationCall = session.call @@ -596,7 +589,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("ReturnCount") private suspend fun validateSession(call: ApplicationCall): Boolean { if (sessionIdGenerator == null) return true @@ -655,7 +647,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("ReturnCount") private fun validateHeaders(call: ApplicationCall): String? { if (!configuration.enableDnsRebindingProtection) return null @@ -680,7 +671,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat return null } - @Suppress("TooGenericExceptionCaught") private suspend fun flushSse(session: ServerSSESession?) { try { session?.send(data = "") @@ -691,7 +681,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("ReturnCount") private suspend fun parseBody(call: ApplicationCall): List? { val maxSize = configuration.maxRequestBodySize val contentLength = call.request.header(HttpHeaders.ContentLength)?.toLongOrNull() ?: 0L @@ -752,7 +741,6 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } - @Suppress("TooGenericExceptionCaught") private suspend fun maybeSendPrimingEvent( streamId: String, session: ServerSSESession?, diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt index c2cb2c4..a05daef 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpKtorServerExtensions.kt @@ -1,5 +1,3 @@ -@file:Suppress("TooManyFunctions") - package io.modelcontextprotocol.kotlin.sdk.server import io.github.oshai.kotlinlogging.KotlinLogging diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractKtorExtensionsTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractKtorExtensionsTest.kt index 28e21b9..7639bc6 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractKtorExtensionsTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractKtorExtensionsTest.kt @@ -14,7 +14,6 @@ import io.ktor.utils.io.readUTF8Line import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities -@Suppress("AbstractClassCanBeConcreteClass") abstract class AbstractKtorExtensionsTest { protected val sseContentType = ContentType("text", "event-stream") diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt index dcbb7a4..32123a6 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransportTest.kt @@ -299,21 +299,18 @@ class StdioServerTransportTest { server.close() } - @Suppress("unused") private fun inputErrors() = listOf( IOException("simulated read failure"), RuntimeException("unexpected read exception"), OutOfMemoryError("unexpected read error"), ) - @Suppress("unused") private fun outputErrors() = listOf( IOException("simulated write failure"), RuntimeException("unexpected write exception"), OutOfMemoryError("unexpected write error"), ) - @Suppress("unused") private fun handlerErrors() = listOf( RuntimeException("handler failure"), IOException("handler IO failure"), diff --git a/kotlin-sdk-testing/build.gradle.kts b/kotlin-sdk-testing/build.gradle.kts index e318f6f..bbb0388 100644 --- a/kotlin-sdk-testing/build.gradle.kts +++ b/kotlin-sdk-testing/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("mcp.multiplatform") + id("mcp.detekt") id("mcp.publishing") id("mcp.dokka") alias(libs.plugins.kotlinx.binary.compatibility.validator) diff --git a/kotlin-sdk-testing/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport.kt b/kotlin-sdk-testing/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport.kt index e6d19f7..ae5f487 100644 --- a/kotlin-sdk-testing/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport.kt +++ b/kotlin-sdk-testing/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransport.kt @@ -27,9 +27,8 @@ import kotlinx.coroutines.yield * communication of JSON-RPC messages. This is useful in scenarios where communication is * message-based and channels are used as the underlying mechanism for delivery. * - * @constructor Initializes the transport with the specified [SendChannel] and [ReceiveChannel]. - * @property sendChannel The channel used to send JSON-RPC messages. - * @property receiveChannel The channel used to receive JSON-RPC messages. + * @param sendChannel The channel used to send JSON-RPC messages. + * @param receiveChannel The channel used to receive JSON-RPC messages. * @param dispatcher The coroutine dispatcher for the event loop. Defaults to [Dispatchers.Default]. */ @ExperimentalMcpApi @@ -75,11 +74,19 @@ public class ChannelTransport( /** * Creates a pair of interconnected [ChannelTransport] objects for bidirectional communication. * - * This method sets up a client and server transport using Kotlin channels, enabling messages sent - * through one transport to be received by the other. It is typically used for testing or in-memory - * communication without external networking. + * Sets up a client and server transport using Kotlin channels, so that messages sent through one + * transport are received by the other. Typically used for testing or in-memory communication + * without external networking. + * + * Example: + * ```kotlin + * val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + * server.connect(serverTransport) + * client.connect(clientTransport) + * ``` * * @param capacity The buffer capacity of the internal channels used for communication. Defaults to 256. + * @param dispatcher The coroutine dispatcher for the event loop. Defaults to [Dispatchers.Default]. * @return A [LinkedTransports] instance containing the client and server transports. */ public fun createLinkedPair( @@ -106,7 +113,6 @@ public class ChannelTransport( override suspend fun initialize() { logger.info { "ChannelTransport starting message processing" } val started = CompletableDeferred() - @Suppress("TooGenericExceptionCaught") scope.launch(CoroutineName("ChannelTransport#${hashCode()}-event-loop")) { try { // Signal that event loop has started, then yield to ensure we're ready diff --git a/kotlin-sdk-testing/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt b/kotlin-sdk-testing/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt index 7fc41bb..4aae932 100644 --- a/kotlin-sdk-testing/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt +++ b/kotlin-sdk-testing/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt @@ -186,7 +186,6 @@ class ChannelTransportTest { val id = ((msg as JSONRPCRequest).id as RequestId.NumberId).value.toInt() received.add(id) if (id == 2) { - @Suppress("TooGenericExceptionThrown") throw RuntimeException("Error processing message 2") } if (received.size == 4) { diff --git a/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt b/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt index 59faee4..13bf272 100644 --- a/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt +++ b/kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt @@ -1,25 +1,3 @@ -@file:Suppress("ktlint:standard:no-empty-class-body", "ktlint:standard:kdoc") -/** - * # MCP Kotlin SDK - * - * A Kotlin Multiplatform implementation of the Model Context Protocol (MCP). - * - * This is the main SDK module that provides a convenient single dependency - * for all MCP functionality including: - * - * - Core protocol types and utilities ([kotlin-sdk-core]) - * - Client implementations ([kotlin-sdk-client]) - * - Server implementations ([kotlin-sdk-server]) - * - * ## Usage - * - * Add this dependency to your project to get access to all MCP Kotlin SDK functionality: - * - * ```kotlin - * implementation("io.modelcontextprotocol:kotlin-sdk:$version") - * ``` - * - * This will transitively include all core, client, and server components. - */ - package io.modelcontextprotocol.kotlin.sdk + +private class Stub diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts index 6e91257..c6baf2d 100644 --- a/test-utils/build.gradle.kts +++ b/test-utils/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("mcp.multiplatform") + id("mcp.detekt") } kotlin { diff --git a/test-utils/detekt-baseline-main.xml b/test-utils/detekt-baseline-main.xml new file mode 100644 index 0000000..431f3d9 --- /dev/null +++ b/test-utils/detekt-baseline-main.xml @@ -0,0 +1,20 @@ + + + + + ForbiddenMethodCall:Retry.kt:RetryExtension$println( "[Retry] Failure on attempt $attempt/$maxAttempts for ${ describeTest( extensionContext, ) }: ${lastError.message}", ) + ForbiddenMethodCall:Retry.kt:RetryExtension$println( "[Retry] Giving up after $attempt attempts for ${ describeTest( extensionContext, ) }: ${lastError.message}", ) + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Adding exports to ${file.absolutePath}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Command failed: ${e.message}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Detected pnpm-specific protocols or workspace. Attempting pnpm install...") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Executing: ${fullCommand.joinToString(" ")}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("Installing TypeScript dependencies in ${typescriptDir.absolutePath}") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("[TS-INSTALL] $it") + ForbiddenMethodCall:TypeScriptRunner.kt:TypeScriptRunner$println("npm install failed. Patching package.json and trying again...") + ForbiddenMethodCall:processUtils.kt:println("Warning: Error reading process error stream: ${e.message}") + ForbiddenMethodCall:processUtils.kt:println("Warning: Error reading process output: ${e.message}") + ForbiddenMethodCall:processUtils.kt:println("[$prefix] $it") + ForbiddenMethodCall:processUtils.kt:println("[$prefix][err] $it") + ForbiddenVoid:Retry.kt:RetryExtension$Void + + diff --git a/test-utils/detekt-baseline.xml b/test-utils/detekt-baseline.xml deleted file mode 100644 index 5abd0b4..0000000 --- a/test-utils/detekt-baseline.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - CyclomaticComplexMethod:Retry.kt:RetryExtension$private fun executeWithRetry - ThrowsCount:Retry.kt:RetryExtension$private fun executeWithRetry - - diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt index a37e6e3..1dcf585 100644 --- a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt @@ -16,7 +16,6 @@ import java.util.Optional @ExtendWith(RetryExtension::class) public annotation class Retry(val times: Int = 3, val delayMs: Long = 1000) -@Suppress("ForbiddenVoid") // needed for Java API interop public class RetryExtension : InvocationInterceptor { override fun interceptTestMethod( invocation: Invocation, diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt index e7f5798..ff9292e 100644 --- a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt @@ -18,7 +18,6 @@ public object TypeScriptRunner { * @param logPrefix Prefix for log lines * @return The started Process */ - @Suppress("LongParameterList") public fun run( typescriptDir: File, scriptPath: String, @@ -54,7 +53,6 @@ public object TypeScriptRunner { * @param typescriptDir The directory containing package.json * @throws RuntimeException if npm install fails */ - @Suppress("ReturnCount") public fun installDependencies(typescriptDir: File) { require(typescriptDir.isDirectory()) { "Type script directory does not exist: ${typescriptDir.absolutePath}" } println("Installing TypeScript dependencies in ${typescriptDir.absolutePath}") diff --git a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt index 71fcfa6..a80c825 100644 --- a/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt @@ -14,7 +14,6 @@ import kotlin.time.Duration.Companion.seconds */ public fun killProcessOnPort(port: Int) { val killCommand = if (isWindows) { - @Suppress("MaxLineLength") listOf( "cmd.exe", "/c",