diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index e071e81b..9186512c 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -66,14 +66,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -83,8 +83,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClien public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getProtocolVersion ()Ljava/lang/String; public final fun getSessionId ()Ljava/lang/String; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun send$default (Lio/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun setProtocolVersion (Ljava/lang/String;)V public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt index d858c1df..0f1353ba 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SSEClientTransport.kt @@ -15,6 +15,7 @@ import io.ktor.http.append import io.ktor.http.isSuccess import io.ktor.http.protocolWithAuthority import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.McpJson import kotlinx.coroutines.CancellationException @@ -98,7 +99,7 @@ public class SseClientTransport( } @OptIn(ExperimentalCoroutinesApi::class) - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { check(initialized.load()) { "SseClientTransport is not initialized!" } check(job?.isActive == true) { "SseClientTransport is closed!" } check(endpoint.isCompleted) { "Not connected!" } 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 6e272eb3..547079fb 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 @@ -4,6 +4,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging import io.modelcontextprotocol.kotlin.sdk.internal.IODispatcher import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import kotlinx.coroutines.CoroutineName @@ -100,7 +101,7 @@ public class StdioClientTransport(private val input: Source, private val output: } } - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (!initialized.load()) { error("Transport not started") } 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 a4028745..bcd1a7cc 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 @@ -22,6 +22,7 @@ import io.ktor.http.contentType import io.ktor.http.isSuccess import io.ktor.utils.io.readUTF8Line import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest @@ -89,8 +90,8 @@ public class StreamableHttpClientTransport( /** * Sends a single message with optional resumption support */ - override suspend fun send(message: JSONRPCMessage) { - send(message, null) + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + send(message, options?.resumptionToken, options?.onResumptionToken) } /** diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockTransport.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockTransport.kt index 860ed147..d7a48efd 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockTransport.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockTransport.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.client import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult @@ -25,7 +26,7 @@ class MockTransport : Transport { override suspend fun start() = Unit - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { mutex.withLock { _sentMessages += message } diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaMockTransport.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaMockTransport.kt index da813c7b..1a0db70b 100644 --- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaMockTransport.kt +++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaMockTransport.kt @@ -8,6 +8,7 @@ import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.JSONRPCResponse import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.shared.Transport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -25,7 +26,7 @@ class OldSchemaMockTransport : Transport { override suspend fun start() = Unit - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { mutex.withLock { _sentMessages += message } diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index def7e8be..28870834 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -433,7 +433,8 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { public final fun getRequestHandlers ()Ljava/util/Map; public final fun getResponseHandlers ()Ljava/util/Map; public final fun getTransport ()Lio/modelcontextprotocol/kotlin/sdk/shared/Transport; - public final fun notification (Lio/modelcontextprotocol/kotlin/sdk/types/Notification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun notification (Lio/modelcontextprotocol/kotlin/sdk/types/Notification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun notification$default (Lio/modelcontextprotocol/kotlin/sdk/shared/Protocol;Lio/modelcontextprotocol/kotlin/sdk/types/Notification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public fun onClose ()V public fun onError (Ljava/lang/Throwable;)V public final fun removeNotificationHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V @@ -476,13 +477,13 @@ public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra public fun ()V } -public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestOptions { - public synthetic fun (Lkotlin/jvm/functions/Function1;JILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lkotlin/jvm/functions/Function1;JLkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Lkotlin/jvm/functions/Function1; - public final fun component2-UwyO8pc ()J - public final fun copy-HG0u8IE (Lkotlin/jvm/functions/Function1;J)Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions; - public static synthetic fun copy-HG0u8IE$default (Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/jvm/functions/Function1;JILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions; +public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestOptions : io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions { + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component4 ()Lkotlin/jvm/functions/Function1; + public final fun component5-UwyO8pc ()J + public final fun copy-9VgGkz4 (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;J)Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions; + public static synthetic fun copy-9VgGkz4$default (Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;JILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions; public fun equals (Ljava/lang/Object;)Z public final fun getOnProgress ()Lkotlin/jvm/functions/Function1; public final fun getTimeout-UwyO8pc ()J @@ -495,16 +496,38 @@ public abstract interface class io/modelcontextprotocol/kotlin/sdk/shared/Transp public abstract fun onClose (Lkotlin/jvm/functions/Function0;)V public abstract fun onError (Lkotlin/jvm/functions/Function1;)V public abstract fun onMessage (Lkotlin/jvm/functions/Function2;)V - public abstract fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun send$default (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public abstract fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class io/modelcontextprotocol/kotlin/sdk/shared/Transport$DefaultImpls { + public static synthetic fun send$default (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public class io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions { + public fun ()V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lkotlin/jvm/functions/Function1; + public fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions; + public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getOnResumptionToken ()Lkotlin/jvm/functions/Function1; + public final fun getRelatedRequestId ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun getResumptionToken ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public abstract class io/modelcontextprotocol/kotlin/sdk/shared/WebSocketMcpTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { public fun ()V public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; protected abstract fun getSession ()Lio/ktor/websocket/WebSocketSession; protected abstract fun initializeSession (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } 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 db8b6efd..7dd04bc0 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 @@ -93,22 +93,58 @@ 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. + * @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. + * @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. */ -public data class RequestOptions( - /** - * If set, requests progress notifications from the remote end (if supported). - * When progress notifications are received, this callback will be invoked. - */ - val onProgress: ProgressCallback? = null, +public class RequestOptions( + relatedRequestId: RequestId? = null, + resumptionToken: String? = null, + onResumptionToken: ((String) -> Unit)? = null, + public val onProgress: ProgressCallback? = null, + public val timeout: Duration = DEFAULT_REQUEST_TIMEOUT, +) : TransportSendOptions(relatedRequestId, resumptionToken, onResumptionToken) { + public operator fun component4(): ProgressCallback? = onProgress + public operator fun component5(): Duration = timeout + + public fun copy( + relatedRequestId: RequestId? = this.relatedRequestId, + resumptionToken: String? = this.resumptionToken, + onResumptionToken: ((String) -> Unit)? = this.onResumptionToken, + onProgress: ProgressCallback? = this.onProgress, + timeout: Duration = this.timeout, + ): RequestOptions = RequestOptions(relatedRequestId, resumptionToken, onResumptionToken, onProgress, timeout) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + if (!super.equals(other)) return false + + other as RequestOptions + + return onProgress == other.onProgress && timeout == other.timeout + } - /** - * A timeout for this request. If exceeded, an McpError with code `RequestTimeout` - * will be raised from request(). - * - * If not specified, `DEFAULT_REQUEST_TIMEOUT` will be used as the timeout. - */ - val timeout: Duration = DEFAULT_REQUEST_TIMEOUT, -) + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (onProgress?.hashCode() ?: 0) + result = 31 * result + timeout.hashCode() + return result + } + + override fun toString(): String = + "RequestOptions(relatedRequestId=$relatedRequestId, resumptionToken=$resumptionToken, onResumptionToken=$onResumptionToken, onProgress=$onProgress, timeout=$timeout)" +} /** * Extra data given to request handlers. @@ -456,11 +492,9 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio ), ) - val serialized = JSONRPCNotification( - notification.method.value, - params = McpJson.encodeToJsonElement(notification), - ) - transport.send(serialized) + val jsonRpcNotification = notification.toJSON() + + transport.send(jsonRpcNotification, options) result.completeExceptionally(reason) } @@ -469,7 +503,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio try { withTimeout(timeout) { logger.trace { "Sending request message with id: $jsonRpcRequestId" } - this@Protocol.transport?.send(jsonRpcRequest) + this@Protocol.transport?.send(jsonRpcRequest, options) } return result.await() } catch (cause: TimeoutCancellationException) { @@ -489,13 +523,14 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio /** * Emits a notification, which is a one-way message that does not expect a response. */ - public suspend fun notification(notification: Notification) { + public suspend fun notification(notification: Notification, relatedRequestId: RequestId? = null) { logger.trace { "Sending notification: ${notification.method}" } val transport = this.transport ?: error("Not connected") assertNotificationCapability(notification.method) + val sendOptions = relatedRequestId?.let { TransportSendOptions(relatedRequestId = it) } + val jsonRpcNotification = notification.toJSON() - val message = notification.toJSON() - transport.send(message) + transport.send(jsonRpcNotification, sendOptions) } /** 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 aa698b40..52f14383 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 @@ -18,8 +18,12 @@ 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. */ - public suspend fun send(message: JSONRPCMessage) + public suspend fun send(message: JSONRPCMessage, options: TransportSendOptions? = null) /** * Closes the connection. diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt new file mode 100644 index 00000000..c56df04e --- /dev/null +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt @@ -0,0 +1,50 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.modelcontextprotocol.kotlin.sdk.types.RequestId + +/** + * Options for sending a JSON-RPC message through transport. + * + * @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. + */ +public open class TransportSendOptions( + public val relatedRequestId: RequestId? = null, + public val resumptionToken: String? = null, + public val onResumptionToken: ((String) -> Unit)? = null, +) { + public operator fun component1(): RequestId? = relatedRequestId + public operator fun component2(): String? = resumptionToken + public operator fun component3(): ((String) -> Unit)? = onResumptionToken + + public open fun copy( + relatedRequestId: RequestId? = this.relatedRequestId, + resumptionToken: String? = this.resumptionToken, + onResumptionToken: ((String) -> Unit)? = this.onResumptionToken, + ): TransportSendOptions = TransportSendOptions(relatedRequestId, resumptionToken, onResumptionToken) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as TransportSendOptions + + return relatedRequestId == other.relatedRequestId && + resumptionToken == other.resumptionToken && + onResumptionToken == other.onResumptionToken + } + + override fun hashCode(): Int { + var result = relatedRequestId?.hashCode() ?: 0 + result = 31 * result + (resumptionToken?.hashCode() ?: 0) + result = 31 * result + (onResumptionToken?.hashCode() ?: 0) + return result + } + + override fun toString(): String = + "TransportSendOptions(relatedRequestId=$relatedRequestId, resumptionToken=$resumptionToken, onResumptionToken=$onResumptionToken)" +} 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 4b209261..392efdcd 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 @@ -90,7 +90,7 @@ public abstract class WebSocketMcpTransport : AbstractTransport() { } } - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { logger.debug { "Sending message" } if (!initialized.load()) { error("Not connected") diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt index f53366c3..142c2afd 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt @@ -166,7 +166,7 @@ private class RecordingTransport : Transport { override suspend fun start() {} - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { sentMessages.send(message) } diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index 11ac161e..8016d3bc 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -136,14 +136,14 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/SseServerTransport public final fun getSessionId ()Ljava/lang/String; public final fun handleMessage (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun handlePostMessage (Lio/ktor/server/application/ApplicationCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class io/modelcontextprotocol/kotlin/sdk/server/StdioServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport { public fun (Lkotlinx/io/Source;Lkotlinx/io/Sink;)V public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun send (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;Lio/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } 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 2e1a2852..c662464d 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 @@ -9,6 +9,7 @@ import io.ktor.server.request.receiveText import io.ktor.server.response.respondText import io.ktor.server.sse.ServerSSESession import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.McpJson import kotlinx.coroutines.InternalCoroutinesApi @@ -119,7 +120,7 @@ public class SseServerTransport(private val endpoint: String, private val sessio _onClose.invoke() } - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (!initialized.load()) { error("Not connected") } 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 b65aecd0..092d0a6f 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 @@ -4,6 +4,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging import io.modelcontextprotocol.kotlin.sdk.internal.IODispatcher import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.ReadBuffer +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.shared.serializeMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import kotlinx.coroutines.CoroutineScope @@ -145,7 +146,7 @@ public class StdioServerTransport(private val inputStream: Source, outputStream: } } - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { writeChannel.send(message) } } diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index b4fbcb70..0726fa82 100644 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -5,6 +5,7 @@ import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.ServerSession import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult @@ -63,7 +64,7 @@ class ClientTest { val clientTransport = object : AbstractTransport() { override suspend fun start() {} - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (message !is JSONRPCRequest) return initialised = true val result = InitializeResult( @@ -109,7 +110,7 @@ class ClientTest { val clientTransport = object : AbstractTransport() { override suspend fun start() {} - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (message !is JSONRPCRequest) return check(message.method == Method.Defined.Initialize.value) @@ -158,7 +159,7 @@ class ClientTest { val clientTransport = object : AbstractTransport() { override suspend fun start() {} - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (message !is JSONRPCRequest) return check(message.method == Method.Defined.Initialize.value) @@ -205,7 +206,7 @@ class ClientTest { val failingTransport = object : AbstractTransport() { override suspend fun start() {} - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (message !is JSONRPCRequest) return check(message.method == Method.Defined.Initialize.value) throw IllegalStateException("Test error") @@ -239,7 +240,7 @@ class ClientTest { val failingTransport = object : AbstractTransport() { override suspend fun start() {} - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (message !is JSONRPCRequest) return check(message.method == Method.Defined.Initialize.value) throw McpException( @@ -277,7 +278,7 @@ class ClientTest { val failingTransport = object : AbstractTransport() { override suspend fun start() {} - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (message !is JSONRPCRequest) return check(message.method == Method.Defined.Initialize.value) throw StreamableHttpError( diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaClientTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaClientTest.kt index 4c3c752e..f9ef4f37 100644 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaClientTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaClientTest.kt @@ -31,6 +31,7 @@ import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.ServerSession import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel @@ -57,7 +58,7 @@ class OldSchemaClientTest { val clientTransport = object : AbstractTransport() { override suspend fun start() {} - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (message !is JSONRPCRequest) return initialised = true val result = InitializeResult( @@ -103,7 +104,7 @@ class OldSchemaClientTest { val clientTransport = object : AbstractTransport() { override suspend fun start() {} - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (message !is JSONRPCRequest) return check(message.method == Method.Defined.Initialize.value) @@ -152,7 +153,7 @@ class OldSchemaClientTest { val clientTransport = object : AbstractTransport() { override suspend fun start() {} - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (message !is JSONRPCRequest) return check(message.method == Method.Defined.Initialize.value) @@ -199,7 +200,7 @@ class OldSchemaClientTest { val failingTransport = object : AbstractTransport() { override suspend fun start() {} - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { if (message !is JSONRPCRequest) return check(message.method == Method.Defined.Initialize.value) throw IllegalStateException("Test error") diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt index 5c5fdd7c..eabdf288 100644 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt @@ -39,7 +39,7 @@ class InMemoryTransport : AbstractTransport() { _onClose.invoke() } - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { val other = otherTransport ?: throw IllegalStateException("Not connected") other._onMessage.invoke(message) diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/OldSchemaInMemoryTransport.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/OldSchemaInMemoryTransport.kt index e7889184..9a0d4d3f 100644 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/OldSchemaInMemoryTransport.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/OldSchemaInMemoryTransport.kt @@ -39,7 +39,7 @@ class OldSchemaInMemoryTransport : AbstractTransport() { _onClose.invoke() } - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { val other = otherTransport ?: throw IllegalStateException("Not connected") other._onMessage.invoke(message) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt index 3aa36bc7..d0056cbb 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinServerForTsClientSse.kt @@ -20,6 +20,7 @@ import io.ktor.server.routing.routing import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation @@ -427,7 +428,7 @@ class HttpServerTransport(private val sessionId: String) : AbstractTransport() { logger.debug { "Starting HTTP server transport for session: $sessionId" } } - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { logger.info { "Sending message: $message" } if (message is JSONRPCResponse) { diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinServerForTsClientSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinServerForTsClientSse.kt index d9a52216..08dcad75 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinServerForTsClientSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinServerForTsClientSse.kt @@ -39,6 +39,7 @@ import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.McpJson +import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions import io.modelcontextprotocol.kotlin.sdk.types.RPCError import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred @@ -428,7 +429,7 @@ class OldSchemaHttpServerTransport(private val sessionId: String) : AbstractTran logger.debug { "Starting HTTP server transport for session: $sessionId" } } - override suspend fun send(message: JSONRPCMessage) { + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { logger.info { "Sending message: $message" } if (message is JSONRPCResponse) {