Skip to content
74 changes: 74 additions & 0 deletions .claude/skills/kdoc/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
name: kdoc
description: "Add KDoc documentation to Kotlin public API. Use whenever the user asks to document Kotlin code, add KDoc, generate API docs, mentions undocumented public declarations, or wants to improve existing documentation. Also trigger when the user says 'add docs', 'document this class/file/module', 'write KDoc', or asks about missing documentation in Kotlin code."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also trigger automatically when adding or updating public facing api. User will simply forget to ask about kdoc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • this violates the solid principles, a skill should have a single responsibility - generating kdoc
  • Automatic invocation triggers do not work that way. You can create a separate orchestration skill for your own use or add a dedicated hook. However, keep in mind that a hook will introduce side effects unless you build a proper workflow around it
  • That is exactly what detekt rules are for, to make sure the user does not forget about this and that such a pr does not get merged

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't violate solid principles, only makes the skill-use condition more explicit. Detekt definitely helps, but it may consume more tokens on the first failing gradle run than this skill consumes.

Did you verify that if you ask the Agent to write a new method or to update the public method without KDoc, the skill is applied and the required KDoc is generated?

Not blocking for this PR but a room for improvement

---

# KDoc Generator

Add KDoc comments to public Kotlin API declarations.

## What to document

All public declarations (no explicit `private`/`internal`/`protected`):
- Classes, interfaces, objects, sealed classes/interfaces
- Functions, extension functions
- Properties
- Enum classes and entries
- Type aliases, annotation classes
Comment on lines +14 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It this looks like too obvious

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not obvious to the agent. In the test eval, the agent missed some points, so I add it explicitly


Include `@Deprecated` elements.

## Context

Read the implementation, not just the signature, to write accurate descriptions. Understanding what the code actually does prevents superficial or misleading documentation.

## KDoc format

**Class/interface example:**

````kotlin
/**
* Manages active client sessions and their lifecycle.
*
* Sessions are created on first connection and cleaned up
* when the transport closes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding example for a class also make sense, e.g. how to construct the class

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that constructor signatures are enough, at least for now, while we are using simple constructors

*
* @property maxSessions upper limit on concurrent sessions
* @property timeout idle timeout before a session is evicted
*/
````

**Function example:**

````kotlin
/**
* Registers a new tool with the given handler.
*
* Example:
* ```kotlin
* server.addTool(
* name = "echo",
* description = "Echoes input back",
* ) { request ->
* CallToolResult(content = listOf(TextContent("Echo: ${request.arguments}")))
* }
* ```
*
* @param name unique tool identifier
* @param description human-readable tool description
* @param handler suspend function invoked when the tool is called
* @return the registered tool definition
*/
````

## Rules

- Summary: concise first sentence starting with a third-person verb ("Creates", "Returns", "Represents"). Expand to 2-3 sentences only when genuinely complex
- **@property** in class-level KDoc for all public properties (never as individual KDoc on the property); **@param** for function parameters
- **@return** for non-Unit return types
- **Example block**: add for DSL builders, complex functions, extension functions with non-obvious usage. Skip for trivial one-liners and simple getters
- **DSL builders**: always include an Example showing full usage with the receiver scope — this is critical for discoverability
- KDoc links (`[ClassName]`, `[methodName]`): only where it adds clear navigational value
- No **@throws** — don't document exceptions
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is questionable. It is very helpful to keep exceptions documented, especially for non-trival use cases

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Exceptions should be documented in non-trivial cases or when custom exceptions are involved. In kotlin-sdk, there is only McpException. it is thrown by two methods, and this is already documented there

- No **suspend** notes — coroutine nature is visible from the signature
- **Existing KDoc**: rewrite if incomplete (missing @param/@return/@property) or low quality
19 changes: 19 additions & 0 deletions config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,22 @@ empty-blocks:
EmptyFunctionBlock:
excludes: *testFolders

comments:
UndocumentedPublicClass:
active: true
excludes: &testAndGeneratedFolders
- '**/test/**'
- '**/commonTest/**'
- '**/jvmTest/**'
- '**/jsTest/**'
- '**/iosTest/**'
- '**/generated-sources/**'
ignoreDefaultCompanionObject: true
UndocumentedPublicFunction:
active: true
excludes: *testAndGeneratedFolders
UndocumentedPublicProperty:
active: true
excludes: *testAndGeneratedFolders
searchProtectedProperty: false
ignoreEnumEntries: true
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version"
private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID"

/**
* Error class for Streamable HTTP transport errors.
* Represents an error from the Streamable HTTP transport.
*
* @property code HTTP status code associated with the error, or `null` if unavailable
*/
public class StreamableHttpError(public val code: Int? = null, message: String? = null) :
Exception("Streamable HTTP error: $message")
Expand All @@ -66,6 +68,9 @@ private sealed interface ConnectResult {
* Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
* It will connect to a server using HTTP POST for sending messages and HTTP GET with Server-Sent Events
* for receiving messages.
*
* @property sessionId session identifier assigned by the server after initialization, or `null` before connection
* @property protocolVersion MCP protocol version negotiated with the server, or `null` before connection
*/
@Suppress("TooManyFunctions")
public class StreamableHttpClientTransport(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package io.modelcontextprotocol.kotlin.sdk.internal

import kotlinx.coroutines.CoroutineDispatcher

/** Platform-specific [CoroutineDispatcher] for I/O-bound operations. */
public expect val IODispatcher: CoroutineDispatcher
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import kotlin.time.Duration.Companion.seconds

private val logger = KotlinLogging.logger { }

/** Default implementation name used in MCP handshake. */
public const val IMPLEMENTATION_NAME: String = "mcp-ktor"

/**
Expand All @@ -51,6 +52,8 @@ public typealias ProgressCallback = (Progress) -> Unit

/**
* Additional initialization options.
*
* @property timeout default timeout for outgoing requests
*/
public open class ProtocolOptions(
/**
Expand Down Expand Up @@ -97,9 +100,13 @@ public class RequestOptions(
public val onProgress: ProgressCallback? = null,
public val timeout: Duration = DEFAULT_REQUEST_TIMEOUT,
) : TransportSendOptions(relatedRequestId, resumptionToken, onResumptionToken) {
/** Destructuring component for [onProgress]. */
public operator fun component4(): ProgressCallback? = onProgress

/** Destructuring component for [timeout]. */
public operator fun component5(): Duration = timeout

/** Creates a copy of this [RequestOptions] with the specified fields replaced. */
public fun copy(
relatedRequestId: RequestId? = this.relatedRequestId,
resumptionToken: String? = this.resumptionToken,
Expand Down Expand Up @@ -139,6 +146,12 @@ internal val COMPLETED = CompletableDeferred(Unit).also { it.complete(Unit) }
/**
* Implements MCP protocol framing on top of a pluggable transport, including
* features like request/response linking, notifications, and progress.
*
* @property transport the active transport, or `null` if not connected
* @property requestHandlers registered request handlers keyed by method name
* @property notificationHandlers registered notification handlers keyed by method name
* @property responseHandlers pending response handlers keyed by request ID
* @property progressHandlers registered progress callbacks keyed by progress token
*/
public abstract class Protocol(@PublishedApi internal val options: ProtocolOptions?) {
public var transport: Transport? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ public open class TransportSendOptions(
public val resumptionToken: String? = null,
public val onResumptionToken: ((String) -> Unit)? = null,
) {
/** Destructuring component for [relatedRequestId]. */
public operator fun component1(): RequestId? = relatedRequestId

/** Destructuring component for [resumptionToken]. */
public operator fun component2(): String? = resumptionToken

/** Destructuring component for [onResumptionToken]. */
public operator fun component3(): ((String) -> Unit)? = onResumptionToken

/** Creates a copy of this [TransportSendOptions] with the specified fields replaced. */
public open fun copy(
relatedRequestId: RequestId? = this.relatedRequestId,
resumptionToken: String? = this.resumptionToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import kotlinx.coroutines.launch
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi

/** WebSocket subprotocol identifier for MCP connections. */
public const val MCP_SUBPROTOCOL: String = "mcp"

private val logger = KotlinLogging.logger {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable

/**
* Represents a ping request used to check if the connection is alive.
*
* @property meta optional request metadata
*/
@Serializable
public data class PingRequest(override val params: BaseRequestParams? = null) :
ClientRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ public data class ClientCapabilities(
public val experimental: JsonObject? = null,
) {

/**
* @property sampling convenience value to enable the sampling capability
* @property elicitation convenience value to enable the elicitation capability
*/
public companion object {
public val sampling: JsonObject = EmptyJsonObject
public val elicitation: JsonObject = EmptyJsonObject
Expand Down Expand Up @@ -100,6 +104,10 @@ public data class ServerCapabilities(
val experimental: JsonObject? = null,
) {

/**
* @property Logging convenience value to enable the logging capability
* @property Completions convenience value to enable the completions capability
*/
public companion object {
public val Logging: JsonObject = EmptyJsonObject
public val Completions: JsonObject = EmptyJsonObject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import kotlinx.serialization.json.JsonObject
// Protocol Version Constants
// ============================================================================

/** The latest supported MCP protocol version string. */
public const val LATEST_PROTOCOL_VERSION: String = "2025-11-25"

/** The default protocol version used when negotiation is not performed. */
public const val DEFAULT_NEGOTIATED_PROTOCOL_VERSION: String = "2025-06-18"

/** All MCP protocol versions supported by this SDK. */
public val SUPPORTED_PROTOCOL_VERSIONS: List<String> = listOf(
LATEST_PROTOCOL_VERSION,
"2025-06-18",
Expand All @@ -25,6 +28,8 @@ public val SUPPORTED_PROTOCOL_VERSIONS: List<String> = listOf(

/**
* Represents an entity that includes additional metadata in its responses.
*
* @property meta optional metadata attached to this entity
*/
@Serializable
public sealed interface WithMeta {
Expand Down Expand Up @@ -123,13 +128,19 @@ public enum class Role {
*
* References are used to point to other entities (prompts, resources, etc.)
* without including their full definitions.
*
* @property type discriminator identifying the reference subtype
*/
@Serializable(with = ReferencePolymorphicSerializer::class)
public sealed interface Reference {
public val type: ReferenceType
}

/** Discriminator for [Reference] subtypes used in completion and other operations. */
/**
* Discriminator for [Reference] subtypes used in completion and other operations.
*
* @property value serialized string representation of this reference type
*/
@Serializable
public enum class ReferenceType(public val value: String) {
@SerialName("ref/prompt")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import kotlinx.serialization.json.JsonObject
* A request from the client to the server to ask for completion options.
*
* @property params The request parameters containing the argument to complete and its context.
* @property meta optional request metadata
*/
@Serializable
public data class CompleteRequest(override val params: CompleteRequestParams) : ClientRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject

/** Discriminator values for the polymorphic [ContentBlock] hierarchy. */
/**
* Discriminator values for the polymorphic [ContentBlock] hierarchy.
*
* @property value serialized string representation of this content type
*/
@Serializable
public enum class ContentTypes(public val value: String) {
@SerialName("text")
Expand All @@ -27,11 +31,19 @@ public enum class ContentTypes(public val value: String) {
EMBEDDED_RESOURCE("resource"),
}

/**
* Base interface for all content blocks in the protocol.
*
* @property type discriminator identifying the content block subtype
*/
@Serializable(with = ContentBlockPolymorphicSerializer::class)
public sealed interface ContentBlock : WithMeta {
public val type: ContentTypes
}

/**
* Content block that carries media data such as text, images, or audio.
*/
@Serializable(with = MediaContentPolymorphicSerializer::class)
public sealed interface MediaContent : ContentBlock

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public data class ElicitRequest(override val params: ElicitRequestParams) : Serv
* Represents the parameters for an `elicitation/create` request.
*
* Implementations: [ElicitRequestFormParams], [ElicitRequestURLParams].
*
* @property message The message to present to the user describing what information is being requested.
*/
@Serializable(with = ElicitRequestParamsSerializer::class)
public sealed interface ElicitRequestParams : RequestParams {
Expand Down Expand Up @@ -111,6 +113,7 @@ public fun ElicitRequestParams(
* later via `tasks/result`.
* @property requestedSchema A restricted subset of JSON Schema. Only top-level properties
* are allowed, without nesting.
* @property mode The elicitation mode discriminator, always `"form"`.
* @property meta Optional metadata. May include a progressToken for out-of-band progress notifications.
*/
@Serializable
Expand Down Expand Up @@ -138,6 +141,7 @@ public data class ElicitRequestFormParams(
* @property task If specified, the caller is requesting task-augmented execution. The request
* will return a [CreateTaskResult] immediately, and the actual result can be retrieved
* later via `tasks/result`.
* @property mode The elicitation mode discriminator, always `"url"`.
* @property meta Optional metadata. May include a progressToken for out-of-band progress notifications.
*/
@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import kotlin.jvm.JvmOverloads
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

/** JSON-RPC protocol version used by MCP (`"2.0"`). */
public const val JSONRPC_VERSION: String = "2.0"

/**
Expand All @@ -40,12 +41,20 @@ public fun RequestId(value: Long): RequestId = RequestId.NumberId(value)
@Serializable(with = RequestIdPolymorphicSerializer::class)
public sealed interface RequestId {

/** A string-based request identifier. */
/**
* A string-based request identifier.
*
* @property value the string representation of this request ID
*/
@JvmInline
@Serializable
public value class StringId(public val value: String) : RequestId

/** A numeric request identifier. */
/**
* A numeric request identifier.
*
* @property value the numeric representation of this request ID
*/
@JvmInline
@Serializable
public value class NumberId(public val value: Long) : RequestId
Expand Down Expand Up @@ -95,12 +104,17 @@ internal fun JSONRPCNotification.fromJSON(): Notification =
* Base interface for all JSON-RPC 2.0 messages.
*
* All messages in the MCP protocol follow the JSON-RPC 2.0 specification.
*
* @property jsonrpc the JSON-RPC protocol version, always `"2.0"`
*/
@Serializable(with = JSONRPCMessagePolymorphicSerializer::class)
public sealed interface JSONRPCMessage {
public val jsonrpc: String
}

/**
* Represents an empty JSON-RPC message used as a placeholder or no-op response.
*/
@Serializable
public data object JSONRPCEmptyMessage : JSONRPCMessage {
override val jsonrpc: String = JSONRPC_VERSION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject

/** Reusable empty [JsonObject] instance. */
public val EmptyJsonObject: JsonObject = JsonObject(emptyMap())

/** Pre-configured [Json] instance for MCP serialization. */
@OptIn(ExperimentalSerializationApi::class)
public val McpJson: Json by lazy {
Json {
Expand Down
Loading
Loading