Skip to content

Commit 2a21439

Browse files
authored
Merge branch 'main' into kpavlov/JSONRPCRequest-constructors
2 parents 6cfd11e + 74c9374 commit 2a21439

File tree

20 files changed

+3066
-21
lines changed

20 files changed

+3066
-21
lines changed

kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult
3434
import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest
3535
import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult
3636
import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel
37+
import io.modelcontextprotocol.kotlin.sdk.types.McpException
3738
import io.modelcontextprotocol.kotlin.sdk.types.Method
3839
import io.modelcontextprotocol.kotlin.sdk.types.PingRequest
3940
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest
@@ -54,6 +55,7 @@ import kotlinx.atomicfu.update
5455
import kotlinx.collections.immutable.minus
5556
import kotlinx.collections.immutable.persistentMapOf
5657
import kotlinx.collections.immutable.toPersistentSet
58+
import kotlinx.serialization.SerializationException
5759
import kotlinx.serialization.json.JsonObject
5860
import kotlin.coroutines.cancellation.CancellationException
5961

@@ -196,11 +198,15 @@ public open class Client(private val clientInfo: Implementation, options: Client
196198
logger.error(error) { "Failed to initialize client: ${error.message}" }
197199
close()
198200

199-
if (error !is CancellationException) {
200-
throw IllegalStateException("Error connecting to transport: ${error.message}", error)
201-
}
201+
when (error) {
202+
is CancellationException,
203+
is McpException,
204+
is StreamableHttpError,
205+
is SerializationException,
206+
-> throw error
202207

203-
throw error
208+
else -> throw IllegalStateException("Error connecting to transport: ${error.message}", error)
209+
}
204210
}
205211
}
206212

kotlin-sdk-core/api/kotlin-sdk-core.api

Lines changed: 250 additions & 0 deletions
Large diffs are not rendered by default.

kotlin-sdk-core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ kotlin {
122122
commonTest {
123123
dependencies {
124124
implementation(kotlin("test"))
125+
implementation(libs.kotlinx.coroutines.test)
125126
implementation(libs.kotest.assertions.core)
126127
implementation(libs.kotest.assertions.json)
127128
}

kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -344,20 +344,24 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
344344
if (handler != null) {
345345
messageId?.let { msg -> _progressHandlers.update { it.remove(msg) } }
346346
} else {
347-
onError(Error("Received a response for an unknown message ID: ${McpJson.encodeToString(response)}"))
347+
onError(
348+
IllegalStateException(
349+
"Received a response for an unknown message ID: ${McpJson.encodeToString(error ?: response)}",
350+
),
351+
)
348352
return
349353
}
350354

351355
if (response != null) {
352356
handler(response, null)
353357
} else {
354358
check(error != null)
355-
val error = McpException(
359+
val mcpException = McpException(
356360
code = error.error.code,
357361
message = error.error.message,
358362
data = error.error.data,
359363
)
360-
handler(null, error)
364+
handler(null, mcpException)
361365
}
362366
}
363367

@@ -403,18 +407,30 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
403407
assertCapabilityForMethod(request.method)
404408
}
405409

406-
val message = request.toJSON()
407-
val messageId = message.id
410+
val jsonRpcRequest = request.toJSON().run {
411+
options?.onProgress?.let { progressHandler ->
412+
logger.trace { "Registering progress handler for request id: $id" }
413+
_progressHandlers.update { current ->
414+
current.put(id, progressHandler)
415+
}
408416

409-
if (options?.onProgress != null) {
410-
logger.trace { "Registering progress handler for request id: $messageId" }
411-
_progressHandlers.update { current ->
412-
current.put(messageId, options.onProgress)
413-
}
417+
val paramsObject = (this.params as? JsonObject) ?: JsonObject(emptyMap())
418+
val metaObject = request.params?.meta?.json ?: JsonObject(emptyMap())
419+
420+
val updatedMeta = JsonObject(
421+
metaObject + ("progressToken" to McpJson.encodeToJsonElement(id)),
422+
)
423+
val updatedParams = JsonObject(
424+
paramsObject + ("_meta" to updatedMeta),
425+
)
426+
427+
this.copy(params = updatedParams)
428+
} ?: this
414429
}
430+
val jsonRpcRequestId = jsonRpcRequest.id
415431

416432
_responseHandlers.update { current ->
417-
current.put(messageId) { response, error ->
433+
current.put(jsonRpcRequestId) { response, error ->
418434
if (error != null) {
419435
result.completeExceptionally(error)
420436
return@put
@@ -430,12 +446,12 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
430446
}
431447

432448
val cancel: suspend (Throwable) -> Unit = { reason: Throwable ->
433-
_responseHandlers.update { current -> current.remove(messageId) }
434-
_progressHandlers.update { current -> current.remove(messageId) }
449+
_responseHandlers.update { current -> current.remove(jsonRpcRequestId) }
450+
_progressHandlers.update { current -> current.remove(jsonRpcRequestId) }
435451

436452
val notification = CancelledNotification(
437453
params = CancelledNotificationParams(
438-
requestId = messageId,
454+
requestId = jsonRpcRequestId,
439455
reason = reason.message ?: "Unknown",
440456
),
441457
)
@@ -452,8 +468,8 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
452468
val timeout = options?.timeout ?: DEFAULT_REQUEST_TIMEOUT
453469
try {
454470
withTimeout(timeout) {
455-
logger.trace { "Sending request message with id: $messageId" }
456-
this@Protocol.transport?.send(message)
471+
logger.trace { "Sending request message with id: $jsonRpcRequestId" }
472+
this@Protocol.transport?.send(jsonRpcRequest)
457473
}
458474
return result.await()
459475
} catch (cause: TimeoutCancellationException) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.modelcontextprotocol.kotlin.sdk.types
2+
3+
/**
4+
* DSL marker annotation for MCP builder classes.
5+
*
6+
* This annotation is used to prevent accidental access to outer DSL scopes
7+
* within nested DSL blocks, ensuring type-safe and unambiguous builder usage.
8+
*
9+
* @see DslMarker
10+
*/
11+
@DslMarker
12+
public annotation class McpDsl
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package io.modelcontextprotocol.kotlin.sdk.types
2+
3+
import kotlinx.serialization.json.JsonObject
4+
import kotlinx.serialization.json.JsonObjectBuilder
5+
import kotlinx.serialization.json.buildJsonObject
6+
7+
/**
8+
* DSL builder for constructing [ClientCapabilities] instances.
9+
*
10+
* This builder is used within [InitializeRequestBuilder] to configure client capabilities.
11+
* All capabilities are optional - the presence of a capability indicates support for that feature.
12+
*
13+
* ## Available Functions (all optional)
14+
* - [sampling] - Indicates support for sampling from an LLM
15+
* - [roots] - Indicates support for listing roots
16+
* - [elicitation] - Indicates support for elicitation from the server
17+
* - [experimental] - Defines experimental, non-standard capabilities
18+
*
19+
* Example usage within [buildInitializeRequest][buildInitializeRequest]:
20+
* ```kotlin
21+
* val request = buildInitializeRequest {
22+
* protocolVersion = "1.0"
23+
* capabilities {
24+
* sampling(ClientCapabilities.sampling)
25+
* roots(listChanged = true)
26+
* experimental {
27+
* put("customFeature", JsonPrimitive(true))
28+
* }
29+
* }
30+
* info("MyClient", "1.0.0")
31+
* }
32+
* ```
33+
*
34+
* @see ClientCapabilities
35+
* @see InitializeRequestBuilder.capabilities
36+
*/
37+
@McpDsl
38+
public class ClientCapabilitiesBuilder @PublishedApi internal constructor() {
39+
private var sampling: JsonObject? = null
40+
private var roots: ClientCapabilities.Roots? = null
41+
private var elicitation: JsonObject? = null
42+
private var experimental: JsonObject? = null
43+
44+
/**
45+
* Indicates that the client supports sampling from an LLM.
46+
*
47+
* Use [ClientCapabilities.sampling] for default empty configuration.
48+
*
49+
* Example:
50+
* ```kotlin
51+
* capabilities {
52+
* sampling(ClientCapabilities.sampling)
53+
* }
54+
* ```
55+
*
56+
* @param value The sampling capability configuration
57+
*/
58+
public fun sampling(value: JsonObject) {
59+
this.sampling = value
60+
}
61+
62+
/**
63+
* Indicates that the client supports sampling from an LLM with custom configuration.
64+
*
65+
* Example:
66+
* ```kotlin
67+
* capabilities {
68+
* sampling {
69+
* put("temperature", JsonPrimitive(0.7))
70+
* }
71+
* }
72+
* ```
73+
*
74+
* @param block Lambda for building the sampling configuration
75+
*/
76+
public fun sampling(block: JsonObjectBuilder.() -> Unit): Unit = sampling(buildJsonObject(block))
77+
78+
/**
79+
* Indicates that the client supports listing roots.
80+
*
81+
* Example with listChanged notification:
82+
* ```kotlin
83+
* capabilities {
84+
* roots(listChanged = true)
85+
* }
86+
* ```
87+
*
88+
* Example without listChanged:
89+
* ```kotlin
90+
* capabilities {
91+
* roots()
92+
* }
93+
* ```
94+
*
95+
* @param listChanged Whether the client will emit notifications when the list of roots changes
96+
*/
97+
public fun roots(listChanged: Boolean? = null) {
98+
this.roots = ClientCapabilities.Roots(listChanged)
99+
}
100+
101+
/**
102+
* Indicates that the client supports elicitation from the server.
103+
*
104+
* Use [ClientCapabilities.elicitation] for default empty configuration.
105+
*
106+
* Example:
107+
* ```kotlin
108+
* capabilities {
109+
* elicitation(ClientCapabilities.elicitation)
110+
* }
111+
* ```
112+
*
113+
* @param value The elicitation capability configuration
114+
*/
115+
public fun elicitation(value: JsonObject) {
116+
this.elicitation = value
117+
}
118+
119+
/**
120+
* Indicates that the client supports elicitation from the server with custom configuration.
121+
*
122+
* Example:
123+
* ```kotlin
124+
* capabilities {
125+
* elicitation {
126+
* put("mode", JsonPrimitive("interactive"))
127+
* }
128+
* }
129+
* ```
130+
*
131+
* @param block Lambda for building the elicitation configuration
132+
*/
133+
public fun elicitation(block: JsonObjectBuilder.() -> Unit): Unit = elicitation(buildJsonObject(block))
134+
135+
/**
136+
* Defines experimental, non-standard capabilities that the client supports.
137+
*
138+
* Example:
139+
* ```kotlin
140+
* capabilities {
141+
* experimental(buildJsonObject {
142+
* put("customFeature", JsonPrimitive(true))
143+
* put("version", JsonPrimitive("1.0"))
144+
* })
145+
* }
146+
* ```
147+
*
148+
* @param value The experimental capabilities configuration
149+
*/
150+
public fun experimental(value: JsonObject) {
151+
this.experimental = value
152+
}
153+
154+
/**
155+
* Defines experimental, non-standard capabilities that the client supports using a DSL builder.
156+
*
157+
* Example:
158+
* ```kotlin
159+
* capabilities {
160+
* experimental {
161+
* put("customFeature", JsonPrimitive(true))
162+
* put("beta", JsonObject(mapOf(
163+
* "enabled" to JsonPrimitive(true),
164+
* "version" to JsonPrimitive("2.0")
165+
* )))
166+
* }
167+
* }
168+
* ```
169+
*
170+
* @param block Lambda for building the experimental capabilities configuration
171+
*/
172+
public fun experimental(block: JsonObjectBuilder.() -> Unit): Unit = experimental(buildJsonObject(block))
173+
174+
@PublishedApi
175+
internal fun build(): ClientCapabilities = ClientCapabilities(
176+
sampling = sampling,
177+
roots = roots,
178+
elicitation = elicitation,
179+
experimental = experimental,
180+
)
181+
}

0 commit comments

Comments
 (0)