Skip to content

Commit dbb6eaa

Browse files
MAERYOdevcrocod
authored andcommitted
test: add comprehensive meta parameter tests for callTool
1 parent 6d951cd commit dbb6eaa

File tree

1 file changed

+270
-0
lines changed

1 file changed

+270
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
package io.modelcontextprotocol.kotlin.sdk.client
2+
3+
import io.modelcontextprotocol.kotlin.sdk.Implementation
4+
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
5+
import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest
6+
import io.modelcontextprotocol.kotlin.sdk.shared.Transport
7+
import kotlinx.coroutines.test.runTest
8+
import kotlinx.serialization.json.*
9+
import kotlin.test.*
10+
11+
/**
12+
* Comprehensive test suite for MCP Client meta parameter functionality
13+
*
14+
* Tests cover:
15+
* - Meta key validation according to MCP specification
16+
* - JSON type conversion for various data types
17+
* - Error handling for invalid meta keys
18+
* - Integration with callTool method
19+
*/
20+
class ClientMetaParameterTest {
21+
22+
private lateinit var client: Client
23+
private lateinit var mockTransport: MockTransport
24+
private val clientInfo = Implementation("test-client", "1.0.0")
25+
26+
@BeforeTest
27+
fun setup() {
28+
mockTransport = MockTransport()
29+
client = Client(clientInfo = clientInfo)
30+
}
31+
32+
@Test
33+
fun `should accept valid meta keys without throwing exception`() = runTest {
34+
val validMeta = buildMap {
35+
put("simple-key", "value1")
36+
put("api.example.com/version", "1.0")
37+
put("com.company.app/setting", "enabled")
38+
put("retry_count", 3)
39+
put("user.preference", true)
40+
// Additional edge cases for valid keys
41+
put("", "empty-name-allowed") // Empty name is allowed per MCP spec
42+
put("valid123", "alphanumeric")
43+
put("multi.dot.name", "multiple-dots")
44+
put("under_score", "underscore")
45+
put("hyphen-dash", "hyphen")
46+
put("org.apache.kafka/consumer-config", "complex-valid-prefix")
47+
}
48+
49+
val result = runCatching {
50+
client.callTool("test-tool", mapOf("arg" to "value"), validMeta)
51+
}
52+
53+
assertTrue(result.isSuccess, "Valid meta keys should not cause exceptions")
54+
}
55+
56+
@Test
57+
fun `should accept edge case valid prefixes and names`() = runTest {
58+
val edgeCaseValidMeta = buildMap {
59+
put("a/", "single-char-prefix-empty-name")
60+
put("a1-b2/test", "alphanumeric-hyphen-prefix")
61+
put("long.domain.name.here/config", "long-prefix")
62+
put("x/a", "minimal-valid-key")
63+
put("test123", "alphanumeric-name-only")
64+
}
65+
66+
val result = runCatching {
67+
client.callTool("test-tool", emptyMap(), edgeCaseValidMeta)
68+
}
69+
70+
assertTrue(result.isSuccess, "Edge case valid meta keys should be accepted")
71+
}
72+
73+
@Test
74+
fun `should reject mcp reserved prefix`() = runTest {
75+
val invalidMeta = mapOf("mcp/internal" to "value")
76+
77+
val exception = assertFailsWith<Error> {
78+
client.callTool("test-tool", emptyMap(), invalidMeta)
79+
}
80+
81+
assertContains(exception.message ?: "", "Invalid _meta key")
82+
}
83+
84+
@Test
85+
fun `should reject modelcontextprotocol reserved prefix`() = runTest {
86+
val invalidMeta = mapOf("modelcontextprotocol/config" to "value")
87+
88+
val exception = assertFailsWith<Error> {
89+
client.callTool("test-tool", emptyMap(), invalidMeta)
90+
}
91+
92+
assertContains(exception.message ?: "", "Invalid _meta key")
93+
}
94+
95+
@Test
96+
fun `should reject nested reserved prefixes`() = runTest {
97+
val invalidKeys = listOf(
98+
"api.mcp.io/setting",
99+
"com.modelcontextprotocol.test/value",
100+
"example.mcp/data",
101+
"subdomain.mcp.com/config",
102+
"app.modelcontextprotocol.dev/setting",
103+
"test.mcp/value",
104+
"service.modelcontextprotocol/data"
105+
)
106+
107+
invalidKeys.forEach { key ->
108+
val exception = assertFailsWith<Error>(
109+
message = "Should reject nested reserved key: $key"
110+
) {
111+
client.callTool("test-tool", emptyMap(), mapOf(key to "value"))
112+
}
113+
assertContains(
114+
charSequence = exception.message ?: "",
115+
other = "Invalid _meta key"
116+
)
117+
}
118+
}
119+
120+
@Test
121+
fun `should reject case-insensitive reserved prefixes`() = runTest {
122+
val invalidKeys = listOf(
123+
"MCP/internal",
124+
"Mcp/config",
125+
"mCp/setting",
126+
"MODELCONTEXTPROTOCOL/data",
127+
"ModelContextProtocol/value",
128+
"modelContextProtocol/test"
129+
)
130+
131+
invalidKeys.forEach { key ->
132+
val exception = assertFailsWith<Error>(
133+
message = "Should reject case-insensitive reserved key: $key"
134+
) {
135+
client.callTool("test-tool", emptyMap(), mapOf(key to "value"))
136+
}
137+
assertContains(
138+
charSequence = exception.message ?: "",
139+
other = "Invalid _meta key"
140+
)
141+
}
142+
}
143+
144+
@Test
145+
fun `should reject invalid key formats`() = runTest {
146+
val invalidKeys = listOf(
147+
"", // empty key
148+
"/invalid", // starts with slash
149+
"invalid/", // ends with slash
150+
"-invalid", // starts with hyphen
151+
".invalid", // starts with dot
152+
"in valid", // contains space
153+
"api../test", // consecutive dots
154+
"api./test" // label ends with dot
155+
)
156+
157+
invalidKeys.forEach { key ->
158+
val exception = assertFailsWith<Error>(
159+
message = "Should reject invalid key format: '$key'"
160+
) {
161+
client.callTool("test-tool", emptyMap(), mapOf(key to "value"))
162+
}
163+
assertContains(
164+
charSequence = exception.message ?: "",
165+
other = "Invalid _meta key"
166+
)
167+
}
168+
}
169+
170+
@Test
171+
fun `should convert various data types to JSON correctly`() = runTest {
172+
val complexMeta = createComplexMetaData()
173+
174+
val result = runCatching {
175+
client.callTool("test-tool", emptyMap(), complexMeta)
176+
}
177+
178+
assertTrue(result.isSuccess, "Complex data type conversion should not throw exceptions")
179+
180+
mockTransport.lastJsonRpcRequest?.let { request ->
181+
assertEquals("tools/call", request.method)
182+
val params = request.params as JsonObject
183+
assertTrue(params.containsKey("_meta"), "Request should contain _meta field")
184+
}
185+
}
186+
187+
@Test
188+
fun `should handle nested map structures correctly`() = runTest {
189+
val nestedMeta = buildNestedConfiguration()
190+
191+
val result = runCatching {
192+
client.callTool("test-tool", emptyMap(), nestedMeta)
193+
}
194+
195+
assertTrue(result.isSuccess)
196+
197+
mockTransport.lastJsonRpcRequest?.let { request ->
198+
val params = request.params as JsonObject
199+
val metaField = params["_meta"] as JsonObject
200+
assertTrue(metaField.containsKey("config"))
201+
}
202+
}
203+
204+
@Test
205+
fun `should include empty meta object when meta parameter not provided`() = runTest {
206+
client.callTool("test-tool", mapOf("arg" to "value"))
207+
208+
mockTransport.lastJsonRpcRequest?.let { request ->
209+
val params = request.params as JsonObject
210+
val metaField = params["_meta"] as JsonObject
211+
assertTrue(metaField.isEmpty(), "Meta field should be empty when not provided")
212+
}
213+
}
214+
215+
private fun createComplexMetaData(): Map<String, Any?> = buildMap {
216+
put("string", "text")
217+
put("number", 42)
218+
put("boolean", true)
219+
put("null_value", null)
220+
put("list", listOf(1, 2, 3))
221+
put("map", mapOf("nested" to "value"))
222+
put("enum", "STRING")
223+
put("int_array", intArrayOf(1, 2, 3))
224+
}
225+
226+
private fun buildNestedConfiguration(): Map<String, Any> = buildMap {
227+
put("config", buildMap {
228+
put("database", buildMap {
229+
put("host", "localhost")
230+
put("port", 5432)
231+
})
232+
put("features", listOf("feature1", "feature2"))
233+
})
234+
}
235+
}
236+
237+
class MockTransport : Transport {
238+
private val _sentMessages = mutableListOf<JSONRPCMessage>()
239+
val sentMessages: List<JSONRPCMessage> = _sentMessages
240+
241+
private var onMessageBlock: (suspend (JSONRPCMessage) -> Unit)? = null
242+
private var onCloseBlock: (() -> Unit)? = null
243+
private var onErrorBlock: ((Throwable) -> Unit)? = null
244+
245+
override suspend fun start() = Unit
246+
247+
override suspend fun send(message: JSONRPCMessage) {
248+
_sentMessages += message
249+
onMessageBlock?.invoke(message)
250+
}
251+
252+
override suspend fun close() {
253+
onCloseBlock?.invoke()
254+
}
255+
256+
override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) {
257+
onMessageBlock = block
258+
}
259+
260+
override fun onClose(block: () -> Unit) {
261+
onCloseBlock = block
262+
}
263+
264+
override fun onError(block: (Throwable) -> Unit) {
265+
onErrorBlock = block
266+
}
267+
}
268+
269+
val MockTransport.lastJsonRpcRequest: JSONRPCRequest?
270+
get() = sentMessages.lastOrNull() as? JSONRPCRequest

0 commit comments

Comments
 (0)