Skip to content

Commit de5f8b5

Browse files
committed
Add MockTransport for testability and improve meta field validation in tests
1 parent 7e087b4 commit de5f8b5

File tree

3 files changed

+130
-85
lines changed

3 files changed

+130
-85
lines changed

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,20 @@ public open class Client(private val clientInfo: Implementation, options: Client
189189
}
190190
}
191191

192-
Method.Defined.PromptsGet, Method.Defined.PromptsList, Method.Defined.CompletionComplete -> {
192+
Method.Defined.PromptsGet,
193+
Method.Defined.PromptsList,
194+
Method.Defined.CompletionComplete,
195+
-> {
193196
if (serverCapabilities?.prompts == null) {
194197
throw IllegalStateException("Server does not support prompts (required for $method)")
195198
}
196199
}
197200

198-
Method.Defined.ResourcesList, Method.Defined.ResourcesTemplatesList,
199-
Method.Defined.ResourcesRead, Method.Defined.ResourcesSubscribe, Method.Defined.ResourcesUnsubscribe,
201+
Method.Defined.ResourcesList,
202+
Method.Defined.ResourcesTemplatesList,
203+
Method.Defined.ResourcesRead,
204+
Method.Defined.ResourcesSubscribe,
205+
Method.Defined.ResourcesUnsubscribe,
200206
-> {
201207
val resCaps = serverCapabilities?.resources
202208
?: error("Server does not support resources (required for $method)")

kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientMetaParameterTest.kt

Lines changed: 27 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
package io.modelcontextprotocol.kotlin.sdk.client
22

3-
import io.modelcontextprotocol.kotlin.sdk.CallToolResult
43
import io.modelcontextprotocol.kotlin.sdk.Implementation
5-
import io.modelcontextprotocol.kotlin.sdk.InitializeResult
6-
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
74
import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest
8-
import io.modelcontextprotocol.kotlin.sdk.JSONRPCResponse
9-
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
10-
import io.modelcontextprotocol.kotlin.sdk.shared.Transport
115
import kotlinx.coroutines.test.runTest
126
import kotlinx.serialization.json.JsonObject
7+
import kotlinx.serialization.json.boolean
8+
import kotlinx.serialization.json.int
9+
import kotlinx.serialization.json.jsonPrimitive
1310
import kotlin.test.BeforeTest
1411
import kotlin.test.Test
1512
import kotlin.test.assertContains
@@ -60,6 +57,26 @@ class ClientMetaParameterTest {
6057
}
6158

6259
assertTrue(result.isSuccess, "Valid meta keys should not cause exceptions")
60+
mockTransport.lastJsonRpcRequest()?.let { request ->
61+
val params = request.params as JsonObject
62+
assertTrue(params.containsKey("_meta"), "Request should contain _meta field")
63+
val metaField = params["_meta"] as JsonObject
64+
65+
// Verify all meta keys are present
66+
assertEquals(validMeta.size, metaField.size, "All meta keys should be included")
67+
68+
// Verify specific key-value pairs
69+
assertEquals("value1", metaField["simple-key"]?.jsonPrimitive?.content)
70+
assertEquals("1.0", metaField["api.example.com/version"]?.jsonPrimitive?.content)
71+
assertEquals("enabled", metaField["com.company.app/setting"]?.jsonPrimitive?.content)
72+
assertEquals(3, metaField["retry_count"]?.jsonPrimitive?.int)
73+
assertEquals(true, metaField["user.preference"]?.jsonPrimitive?.boolean)
74+
assertEquals("alphanumeric", metaField["valid123"]?.jsonPrimitive?.content)
75+
assertEquals("multiple-dots", metaField["multi.dot.name"]?.jsonPrimitive?.content)
76+
assertEquals("underscore", metaField["under_score"]?.jsonPrimitive?.content)
77+
assertEquals("hyphen", metaField["hyphen-dash"]?.jsonPrimitive?.content)
78+
assertEquals("complex-valid-prefix", metaField["org.apache.kafka/consumer-config"]?.jsonPrimitive?.content)
79+
}
6380
}
6481

6582
@Test
@@ -191,7 +208,7 @@ class ClientMetaParameterTest {
191208

192209
assertTrue(result.isSuccess, "Complex data type conversion should not throw exceptions")
193210

194-
mockTransport.lastJsonRpcRequest?.let { request ->
211+
mockTransport.lastJsonRpcRequest()?.let { request ->
195212
assertEquals("tools/call", request.method)
196213
val params = request.params as JsonObject
197214
assertTrue(params.containsKey("_meta"), "Request should contain _meta field")
@@ -208,7 +225,7 @@ class ClientMetaParameterTest {
208225

209226
assertTrue(result.isSuccess)
210227

211-
mockTransport.lastJsonRpcRequest?.let { request ->
228+
mockTransport.lastJsonRpcRequest()?.let { request ->
212229
val params = request.params as JsonObject
213230
val metaField = params["_meta"] as JsonObject
214231
assertTrue(metaField.containsKey("config"))
@@ -219,7 +236,7 @@ class ClientMetaParameterTest {
219236
fun `should include empty meta object when meta parameter not provided`() = runTest {
220237
client.callTool("test-tool", mapOf("arg" to "value"))
221238

222-
mockTransport.lastJsonRpcRequest?.let { request ->
239+
mockTransport.lastJsonRpcRequest()?.let { request ->
223240
val params = request.params as JsonObject
224241
val metaField = params["_meta"] as JsonObject
225242
assertTrue(metaField.isEmpty(), "Meta field should be empty when not provided")
@@ -254,76 +271,4 @@ class ClientMetaParameterTest {
254271
}
255272
}
256273

257-
class MockTransport : Transport {
258-
private val _sentMessages = mutableListOf<JSONRPCMessage>()
259-
val sentMessages: List<JSONRPCMessage> = _sentMessages
260-
261-
private var onMessageBlock: (suspend (JSONRPCMessage) -> Unit)? = null
262-
private var onCloseBlock: (() -> Unit)? = null
263-
private var onErrorBlock: ((Throwable) -> Unit)? = null
264-
265-
override suspend fun start() = Unit
266-
267-
override suspend fun send(message: JSONRPCMessage) {
268-
_sentMessages += message
269-
270-
// Auto-respond to initialization and tool calls
271-
when (message) {
272-
is JSONRPCRequest -> {
273-
when (message.method) {
274-
"initialize" -> {
275-
val initResponse = JSONRPCResponse(
276-
id = message.id,
277-
result = InitializeResult(
278-
protocolVersion = "2024-11-05",
279-
capabilities = ServerCapabilities(
280-
tools = ServerCapabilities.Tools(listChanged = null),
281-
),
282-
serverInfo = Implementation("mock-server", "1.0.0"),
283-
),
284-
)
285-
onMessageBlock?.invoke(initResponse)
286-
}
287-
288-
"tools/call" -> {
289-
val toolResponse = JSONRPCResponse(
290-
id = message.id,
291-
result = CallToolResult(
292-
content = listOf(),
293-
isError = false,
294-
),
295-
)
296-
onMessageBlock?.invoke(toolResponse)
297-
}
298-
}
299-
}
300-
301-
else -> {
302-
// Handle other message types if needed
303-
}
304-
}
305-
}
306-
307-
override suspend fun close() {
308-
onCloseBlock?.invoke()
309-
}
310-
311-
override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) {
312-
onMessageBlock = block
313-
}
314-
315-
override fun onClose(block: () -> Unit) {
316-
onCloseBlock = block
317-
}
318-
319-
override fun onError(block: (Throwable) -> Unit) {
320-
onErrorBlock = block
321-
}
322-
323-
fun setupInitializationResponse() {
324-
// This method helps set up the mock for proper initialization
325-
}
326-
}
327-
328-
val MockTransport.lastJsonRpcRequest: JSONRPCRequest?
329-
get() = sentMessages.lastOrNull() as? JSONRPCRequest
274+
suspend fun MockTransport.lastJsonRpcRequest(): JSONRPCRequest? = getSentMessages().lastOrNull() as? JSONRPCRequest
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package io.modelcontextprotocol.kotlin.sdk.client
2+
3+
import io.modelcontextprotocol.kotlin.sdk.CallToolResult
4+
import io.modelcontextprotocol.kotlin.sdk.Implementation
5+
import io.modelcontextprotocol.kotlin.sdk.InitializeResult
6+
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
7+
import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest
8+
import io.modelcontextprotocol.kotlin.sdk.JSONRPCResponse
9+
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
10+
import io.modelcontextprotocol.kotlin.sdk.shared.Transport
11+
import kotlinx.coroutines.sync.Mutex
12+
import kotlinx.coroutines.sync.withLock
13+
14+
class MockTransport : Transport {
15+
private val _sentMessages = mutableListOf<JSONRPCMessage>()
16+
private val _receivedMessages = mutableListOf<JSONRPCMessage>()
17+
private val mutex = Mutex()
18+
19+
suspend fun getSentMessages() = mutex.withLock { _sentMessages.toList() }
20+
suspend fun getReceivedMessages() = mutex.withLock { _receivedMessages.toList() }
21+
22+
private var onMessageBlock: (suspend (JSONRPCMessage) -> Unit)? = null
23+
private var onCloseBlock: (() -> Unit)? = null
24+
private var onErrorBlock: ((Throwable) -> Unit)? = null
25+
26+
override suspend fun start() = Unit
27+
28+
override suspend fun send(message: JSONRPCMessage) {
29+
mutex.withLock {
30+
_sentMessages += message
31+
}
32+
33+
// Auto-respond to initialization and tool calls
34+
when (message) {
35+
is JSONRPCRequest -> {
36+
when (message.method) {
37+
"initialize" -> {
38+
val initResponse = JSONRPCResponse(
39+
id = message.id,
40+
result = InitializeResult(
41+
protocolVersion = "2024-11-05",
42+
capabilities = ServerCapabilities(
43+
tools = ServerCapabilities.Tools(listChanged = null),
44+
),
45+
serverInfo = Implementation("mock-server", "1.0.0"),
46+
),
47+
)
48+
onMessageBlock?.invoke(initResponse)
49+
}
50+
51+
"tools/call" -> {
52+
val toolResponse = JSONRPCResponse(
53+
id = message.id,
54+
result = CallToolResult(
55+
content = listOf(),
56+
isError = false,
57+
),
58+
)
59+
onMessageBlock?.invoke(toolResponse)
60+
}
61+
}
62+
}
63+
64+
else -> {
65+
// Handle other message types if needed
66+
}
67+
}
68+
}
69+
70+
override suspend fun close() {
71+
onCloseBlock?.invoke()
72+
}
73+
74+
override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) {
75+
onMessageBlock = { message ->
76+
mutex.withLock {
77+
_receivedMessages += message
78+
}
79+
block(message)
80+
}
81+
}
82+
83+
override fun onClose(block: () -> Unit) {
84+
onCloseBlock = block
85+
}
86+
87+
override fun onError(block: (Throwable) -> Unit) {
88+
onErrorBlock = block
89+
}
90+
91+
fun setupInitializationResponse() {
92+
// This method helps set up the mock for proper initialization
93+
}
94+
}

0 commit comments

Comments
 (0)