Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ kover {
}
verify {
rule {
minBound(65)
minBound(75)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

rise the bar

}
}
}
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ kotlinx-io = "0.8.0"
ktor = "3.2.3"
logging = "7.0.13"
slf4j = "2.0.17"
kotest = "6.0.4"
kotest = "5.9.1" # for JVM 1.8
awaitility = "4.3.0"
mokksy = "0.6.1"

Expand Down
10 changes: 8 additions & 2 deletions kotlin-sdk-core/api/kotlin-sdk-core.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
public final class io/modelcontextprotocol/kotlin/sdk/Annotations {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Annotations$Companion;
public fun <init> ()V
public fun <init> (Ljava/util/List;Lkotlin/time/Instant;Ljava/lang/Double;)V
public synthetic fun <init> (Ljava/util/List;Lkotlin/time/Instant;Ljava/lang/Double;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/util/List;
public final fun component2 ()Lkotlin/time/Instant;
public final fun component3 ()Ljava/lang/Double;
Expand Down Expand Up @@ -1589,12 +1591,11 @@ public final class io/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class io/modelcontextprotocol/kotlin/sdk/McpError : java/lang/Exception {
public final class io/modelcontextprotocol/kotlin/sdk/McpException : java/lang/Exception {
public fun <init> (ILjava/lang/String;Lkotlinx/serialization/json/JsonObject;)V
public synthetic fun <init> (ILjava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getCode ()I
public final fun getData ()Lkotlinx/serialization/json/JsonObject;
public fun getMessage ()Ljava/lang/String;
}

public abstract interface class io/modelcontextprotocol/kotlin/sdk/Method {
Expand Down Expand Up @@ -1699,7 +1700,9 @@ public final class io/modelcontextprotocol/kotlin/sdk/ModelHint$Companion {

public final class io/modelcontextprotocol/kotlin/sdk/ModelPreferences {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ModelPreferences$Companion;
public fun <init> ()V
public fun <init> (Ljava/util/List;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;)V
public synthetic fun <init> (Ljava/util/List;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Double;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getCostPriority ()Ljava/lang/Double;
public final fun getHints ()Ljava/util/List;
public final fun getIntelligencePriority ()Ljava/lang/Double;
Expand Down Expand Up @@ -1883,6 +1886,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/ProgressNotification$Param
public final class io/modelcontextprotocol/kotlin/sdk/Prompt {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Prompt$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getArguments ()Ljava/util/List;
public final fun getDescription ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
Expand Down Expand Up @@ -2472,6 +2476,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/Role$Companion {
public final class io/modelcontextprotocol/kotlin/sdk/Root {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/Root$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/Root;
Expand Down Expand Up @@ -2995,6 +3000,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/Tool$Output$Companion {

public final class io/modelcontextprotocol/kotlin/sdk/ToolAnnotations {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/ToolAnnotations$Companion;
public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@file:Suppress("unused", "EnumEntryName")

package io.modelcontextprotocol.kotlin.sdk

import kotlinx.serialization.json.JsonObject

@Deprecated("Use McpException instead", ReplaceWith("McpException"))
public typealias McpError = McpException

/**
* Represents an error specific to the MCP protocol.
*
* @property code The error code.
* @property message The error message.
* @property data Additional error data as a JSON object.
*/
public class McpException(public val code: Int, message: String, public val data: JsonObject = EmptyJsonObject) :
Exception("MCP error $code: \"$message\"")

/**
* Converts a `JSONRPCError` instance to an [McpException] instance.
*
* @return An [McpException] containing the code, message, and data from the `JSONRPCError`.
*/
internal fun JSONRPCError.toMcpException(): McpException = McpException(
code = this.code.code,
message = this.message,
data = this.data,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.modelcontextprotocol.kotlin.sdk.shared

import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
import kotlinx.coroutines.CompletableDeferred

/**
* Implements [onClose], [onError] and [onMessage] functions of [Transport] providing
* corresponding [_onClose], [_onError] and [_onMessage] properties to use for an implementation.
*/
@Suppress("PropertyName")
public abstract class AbstractTransport : Transport {
protected var _onClose: (() -> Unit) = {}
private set
protected var _onError: ((Throwable) -> Unit) = {}
private set

// to not skip messages
private val _onMessageInitialized = CompletableDeferred<Unit>()
protected var _onMessage: (suspend ((JSONRPCMessage) -> Unit)) = {
_onMessageInitialized.await()
_onMessage.invoke(it)
}
private set

override fun onClose(block: () -> Unit) {
val old = _onClose
_onClose = {
old()
block()
}
}

override fun onError(block: (Throwable) -> Unit) {
val old = _onError
_onError = { e ->
old(e)
block(e)
}
}

override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) {
val old: suspend (JSONRPCMessage) -> Unit = when (_onMessageInitialized.isCompleted) {
true -> _onMessage
false -> { _ -> }
}

_onMessage = { message ->
old(message)
block(message)
}

_onMessageInitialized.complete(Unit)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import io.modelcontextprotocol.kotlin.sdk.JSONRPCError
import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification
import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest
import io.modelcontextprotocol.kotlin.sdk.JSONRPCResponse
import io.modelcontextprotocol.kotlin.sdk.McpError
import io.modelcontextprotocol.kotlin.sdk.McpException
import io.modelcontextprotocol.kotlin.sdk.Method
import io.modelcontextprotocol.kotlin.sdk.Notification
import io.modelcontextprotocol.kotlin.sdk.PingRequest
Expand All @@ -19,6 +19,7 @@ import io.modelcontextprotocol.kotlin.sdk.RequestId
import io.modelcontextprotocol.kotlin.sdk.RequestResult
import io.modelcontextprotocol.kotlin.sdk.fromJSON
import io.modelcontextprotocol.kotlin.sdk.toJSON
import io.modelcontextprotocol.kotlin.sdk.toMcpException
import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.getAndUpdate
Expand Down Expand Up @@ -97,7 +98,7 @@ public data class RequestOptions(
val onProgress: ProgressCallback? = null,

/**
* A timeout for this request. If exceeded, an McpError with code `RequestTimeout`
* A timeout for this request. If exceeded, an [McpException] with code `RequestTimeout`
* will be raised from request().
*
* If not specified, `DEFAULT_REQUEST_TIMEOUT` will be used as the timeout.
Expand All @@ -116,6 +117,7 @@ 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.
*/
@Suppress("TooManyFunctions")
public abstract class Protocol(@PublishedApi internal val options: ProtocolOptions?) {
public var transport: Transport? = null
private set
Expand Down Expand Up @@ -190,7 +192,9 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
/**
* Attaches to the given transport, starts it, and starts listening for messages.
*
* The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
* The Protocol object assumes ownership of the Transport,
* replacing any callbacks that have already been set,
* and expects that it is the only user of the Transport instance going forward.
*/
public open suspend fun connect(transport: Transport) {
this.transport = transport
Expand Down Expand Up @@ -222,7 +226,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
transport = null
onClose()

val error = McpError(ErrorCode.Defined.ConnectionClosed.code, "Connection closed")
val error = McpException(ErrorCode.Defined.ConnectionClosed.code, "Connection closed")
for (handler in handlersToNotify) {
handler(null, error)
}
Expand All @@ -237,6 +241,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
logger.trace { "No handler found for notification: ${notification.method}" }
return
}
@Suppress("TooGenericExceptionCaught")
try {
handler(notification)
} catch (cause: Throwable) {
Expand All @@ -252,6 +257,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio

if (handler === null) {
logger.trace { "No handler found for request: ${request.method}" }
@Suppress("TooGenericExceptionCaught")
try {
transport?.send(
JSONRPCResponse(
Expand All @@ -268,7 +274,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
}
return
}

@Suppress("TooGenericExceptionCaught")
try {
val result = handler(request, RequestHandlerExtra())
logger.trace { "Request handled successfully: ${request.method} (id: ${request.id})" }
Expand Down Expand Up @@ -303,7 +309,8 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio

private fun onProgress(notification: ProgressNotification) {
logger.trace {
"Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}"
"Received progress notification: token=${notification.params.progressToken}, " +
"progress=${notification.params.progress}/${notification.params.total}"
}
val progress = notification.params.progress
val total = notification.params.total
Expand Down Expand Up @@ -347,7 +354,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
handler(response, null)
} else {
check(error != null)
val error = McpError(
val error = McpException(
error.code.code,
error.message,
error.data,
Expand Down Expand Up @@ -392,7 +399,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
public suspend fun <T : RequestResult> request(request: Request, options: RequestOptions? = null): T {
logger.trace { "Sending request: ${request.method}" }
val result = CompletableDeferred<T>()
val transport = transport ?: throw Error("Not connected")
val transport = transport ?: throw McpException(ErrorCode.Defined.ConnectionClosed.code, "Not connected")

if ([email protected]?.enforceStrictCapabilities == true) {
assertCapabilityForMethod(request.method)
Expand All @@ -415,11 +422,12 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
return@put
}

if (response?.error != null) {
result.completeExceptionally(IllegalStateException(response.error.toString()))
response?.error?.let {
result.completeExceptionally(it.toMcpException())
return@put
}

@Suppress("TooGenericExceptionCaught")
try {
@Suppress("UNCHECKED_CAST")
result.complete(response!!.result as T)
Expand Down Expand Up @@ -459,7 +467,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
} catch (cause: TimeoutCancellationException) {
logger.error { "Request timed out after ${timeout.inWholeMilliseconds}ms: ${request.method}" }
cancel(
McpError(
McpException(
ErrorCode.Defined.RequestTimeout.code,
"Request timed out",
JsonObject(mutableMapOf("timeout" to JsonPrimitive(timeout.inWholeMilliseconds))),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.modelcontextprotocol.kotlin.sdk.shared

import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
import kotlinx.coroutines.CompletableDeferred

/**
* Describes the minimal contract for MCP transport that a client or server can communicate over.
Expand Down Expand Up @@ -47,53 +46,3 @@ public interface Transport {
*/
public fun onMessage(block: suspend (JSONRPCMessage) -> Unit)
}

/**
* Implements [onClose], [onError] and [onMessage] functions of [Transport] providing
* corresponding [_onClose], [_onError] and [_onMessage] properties to use for an implementation.
*/
@Suppress("PropertyName")
public abstract class AbstractTransport : Transport {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved to separate file

protected var _onClose: (() -> Unit) = {}
private set
protected var _onError: ((Throwable) -> Unit) = {}
private set

// to not skip messages
private val _onMessageInitialized = CompletableDeferred<Unit>()
protected var _onMessage: (suspend ((JSONRPCMessage) -> Unit)) = {
_onMessageInitialized.await()
_onMessage.invoke(it)
}
private set

override fun onClose(block: () -> Unit) {
val old = _onClose
_onClose = {
old()
block()
}
}

override fun onError(block: (Throwable) -> Unit) {
val old = _onError
_onError = { e ->
old(e)
block(e)
}
}

override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) {
val old: suspend (JSONRPCMessage) -> Unit = when (_onMessageInitialized.isCompleted) {
true -> _onMessage
false -> { _ -> }
}

_onMessage = { message ->
old(message)
block(message)
}

_onMessageInitialized.complete(Unit)
}
}
Loading
Loading