Skip to content

Commit f927aa4

Browse files
committed
fixup! Add MCP conformance test coverage
1 parent c8a4a86 commit f927aa4

File tree

3 files changed

+99
-80
lines changed

3 files changed

+99
-80
lines changed

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ fun main(args: Array<String>) {
3232
val client = Client(
3333
clientInfo = Implementation(
3434
name = "kotlin-conformance-client",
35-
version = "1.0.0"
36-
)
35+
version = "1.0.0",
36+
),
3737
)
3838

3939
var exitCode = 0
@@ -57,9 +57,9 @@ fun main(args: Array<String>) {
5757
arguments = buildJsonObject {
5858
put("a", JsonPrimitive(5))
5959
put("b", JsonPrimitive(3))
60-
}
61-
)
62-
)
60+
},
61+
),
62+
),
6363
)
6464
logger.info { "Tool result: ${result.content}" }
6565
} else if (tools.tools.isNotEmpty()) {
@@ -72,9 +72,9 @@ fun main(args: Array<String>) {
7272
name = toolName,
7373
arguments = buildJsonObject {
7474
put("input", JsonPrimitive("test"))
75-
}
76-
)
77-
)
75+
},
76+
),
77+
),
7878
)
7979
logger.info { "Tool result: ${result.content}" }
8080
}

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import kotlinx.coroutines.CoroutineScope
4444
import kotlinx.coroutines.Dispatchers
4545
import kotlinx.coroutines.channels.Channel
4646
import kotlinx.coroutines.launch
47-
import kotlinx.coroutines.runBlocking
4847
import kotlinx.coroutines.withTimeoutOrNull
4948
import kotlinx.serialization.json.Json
5049
import kotlinx.serialization.json.JsonElement
@@ -108,13 +107,16 @@ fun main(args: Array<String>) {
108107
JsonObject.serializer(),
109108
buildJsonObject {
110109
put("jsonrpc", "2.0")
111-
put("error", buildJsonObject {
112-
put("code", -32700)
113-
put("message", "Parse error: ${e.message}")
114-
})
110+
put(
111+
"error",
112+
buildJsonObject {
113+
put("code", -32700)
114+
put("message", "Parse error: ${e.message}")
115+
},
116+
)
115117
put("id", JsonNull)
116-
}
117-
)
118+
},
119+
),
118120
)
119121
return@post
120122
}
@@ -158,13 +160,16 @@ fun main(args: Array<String>) {
158160
JsonObject.serializer(),
159161
buildJsonObject {
160162
put("jsonrpc", "2.0")
161-
put("error", buildJsonObject {
162-
put("code", -32000)
163-
put("message", "Bad Request: No valid session ID provided")
164-
})
163+
put(
164+
"error",
165+
buildJsonObject {
166+
put("code", -32000)
167+
put("message", "Bad Request: No valid session ID provided")
168+
},
169+
)
165170
put("id", JsonNull)
166-
}
167-
)
171+
},
172+
),
168173
)
169174
}
170175
}
@@ -191,46 +196,49 @@ private fun createConformanceServer(): Server {
191196
val server = Server(
192197
Implementation(
193198
name = "kotlin-conformance-server",
194-
version = "1.0.0"
199+
version = "1.0.0",
195200
),
196201
ServerOptions(
197202
capabilities = ServerCapabilities(
198203
tools = ServerCapabilities.Tools(listChanged = true),
199204
resources = ServerCapabilities.Resources(subscribe = true, listChanged = true),
200-
prompts = ServerCapabilities.Prompts(listChanged = true)
201-
)
202-
)
205+
prompts = ServerCapabilities.Prompts(listChanged = true),
206+
),
207+
),
203208
)
204209

205210
server.addTool(
206211
name = "test_tool",
207212
description = "A test tool for conformance testing",
208213
inputSchema = ToolSchema(
209214
properties = buildJsonObject {
210-
put("input", buildJsonObject {
211-
put("type", "string")
212-
put("description", "Test input parameter")
213-
})
215+
put(
216+
"input",
217+
buildJsonObject {
218+
put("type", "string")
219+
put("description", "Test input parameter")
220+
},
221+
)
214222
},
215-
required = listOf("input")
216-
)
223+
required = listOf("input"),
224+
),
217225
) { request ->
218226
val input = (request.params.arguments?.get("input") as? JsonPrimitive)?.content ?: "no input"
219227
CallToolResult(
220-
content = listOf(TextContent("Tool executed with input: $input"))
228+
content = listOf(TextContent("Tool executed with input: $input")),
221229
)
222230
}
223231

224232
server.addResource(
225233
uri = "test://resource",
226234
name = "Test Resource",
227235
description = "A test resource for conformance testing",
228-
mimeType = "text/plain"
236+
mimeType = "text/plain",
229237
) { request ->
230238
ReadResourceResult(
231239
contents = listOf(
232-
TextResourceContents("Test resource content", request.params.uri, "text/plain")
233-
)
240+
TextResourceContents("Test resource content", request.params.uri, "text/plain"),
241+
),
234242
)
235243
}
236244

@@ -241,18 +249,18 @@ private fun createConformanceServer(): Server {
241249
PromptArgument(
242250
name = "arg",
243251
description = "Test argument",
244-
required = false
245-
)
246-
)
252+
required = false,
253+
),
254+
),
247255
) {
248256
GetPromptResult(
249257
messages = listOf(
250258
PromptMessage(
251259
role = Role.User,
252-
content = TextContent("Test prompt content")
253-
)
260+
content = TextContent("Test prompt content"),
261+
),
254262
),
255-
description = "Test prompt description"
263+
description = "Test prompt description",
256264
)
257265
}
258266

@@ -310,10 +318,10 @@ private class HttpServerTransport(private val sessionId: String) : AbstractTrans
310318
McpJson.encodeToString(
311319
JSONRPCError(
312320
message.id,
313-
RPCError(RPCError.ErrorCode.REQUEST_TIMEOUT, "Request timed out")
314-
)
321+
RPCError(RPCError.ErrorCode.REQUEST_TIMEOUT, "Request timed out"),
322+
),
315323
),
316-
ContentType.Application.Json
324+
ContentType.Application.Json,
317325
)
318326
}
319327
}
@@ -329,11 +337,11 @@ private class HttpServerTransport(private val sessionId: String) : AbstractTrans
329337
McpJson.encodeToString(
330338
JSONRPCError(
331339
RequestId(0),
332-
RPCError(RPCError.ErrorCode.INTERNAL_ERROR, "Internal error: ${e.message}")
333-
)
340+
RPCError(RPCError.ErrorCode.INTERNAL_ERROR, "Internal error: ${e.message}"),
341+
),
334342
),
335343
ContentType.Application.Json,
336-
HttpStatusCode.InternalServerError
344+
HttpStatusCode.InternalServerError,
337345
)
338346
}
339347
}

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTest.kt

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,11 @@ class ConformanceTest {
5151
private const val GRACEFUL_SHUTDOWN_SECONDS = 5L
5252
private const val FORCE_SHUTDOWN_SECONDS = 2L
5353

54-
private fun findFreePort(): Int {
55-
return ServerSocket(0).use { it.localPort }
56-
}
54+
private fun findFreePort(): Int = ServerSocket(0).use { it.localPort }
5755

58-
private fun getRuntimeClasspath(): String {
59-
return ManagementFactory.getRuntimeMXBean().classPath
60-
}
56+
private fun getRuntimeClasspath(): String = ManagementFactory.getRuntimeMXBean().classPath
6157

62-
private fun getTestClasspath(): String {
63-
return System.getProperty("test.classpath") ?: getRuntimeClasspath()
64-
}
58+
private fun getTestClasspath(): String = System.getProperty("test.classpath") ?: getRuntimeClasspath()
6559

6660
private fun waitForServerReady(
6761
url: String,
@@ -104,9 +98,10 @@ class ConformanceTest {
10498

10599
val processBuilder = ProcessBuilder(
106100
"java",
107-
"-cp", getRuntimeClasspath(),
101+
"-cp",
102+
getRuntimeClasspath(),
108103
"io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceServerKt",
109-
serverPort.toString()
104+
serverPort.toString(),
110105
)
111106

112107
val process = processBuilder.start()
@@ -134,14 +129,14 @@ class ConformanceTest {
134129

135130
if (!serverReady) {
136131
val errorInfo = if (serverErrorOutput.isNotEmpty()) {
137-
"\n\nServer error output:\n${serverErrorOutput}"
132+
"\n\nServer error output:\n$serverErrorOutput"
138133
} else {
139134
""
140135
}
141136
serverProcess?.destroyForcibly()
142137
throw IllegalStateException(
143138
"Server failed to start within $DEFAULT_SERVER_STARTUP_TIMEOUT_SECONDS seconds. " +
144-
"Check if port $serverPort is available.$errorInfo"
139+
"Check if port $serverPort is available.$errorInfo",
145140
)
146141
}
147142

@@ -184,11 +179,9 @@ class ConformanceTest {
184179
}
185180

186181
@TestFactory
187-
fun `MCP Client Conformance Tests`(): List<DynamicTest> {
188-
return CLIENT_SCENARIOS.map { scenario ->
189-
DynamicTest.dynamicTest("Client: $scenario") {
190-
runClientConformanceTest(scenario)
191-
}
182+
fun `MCP Client Conformance Tests`(): List<DynamicTest> = CLIENT_SCENARIOS.map { scenario ->
183+
DynamicTest.dynamicTest("Client: $scenario") {
184+
runClientConformanceTest(scenario)
192185
}
193186
}
194187

@@ -197,8 +190,10 @@ class ConformanceTest {
197190
"npx",
198191
"@modelcontextprotocol/conformance",
199192
"server",
200-
"--url", serverUrl,
201-
"--scenario", scenario
193+
"--url",
194+
serverUrl,
195+
"--scenario",
196+
scenario,
202197
).apply {
203198
inheritIO()
204199
}
@@ -211,28 +206,27 @@ class ConformanceTest {
211206

212207
val clientCommand = listOf(
213208
"java",
214-
"-cp", testClasspath,
215-
"io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceClientKt"
209+
"-cp",
210+
testClasspath,
211+
"io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceClientKt",
216212
)
217213

218214
val processBuilder = ProcessBuilder(
219215
"npx",
220216
"@modelcontextprotocol/conformance",
221217
"client",
222-
"--command", clientCommand.joinToString(" "),
223-
"--scenario", scenario
218+
"--command",
219+
clientCommand.joinToString(" "),
220+
"--scenario",
221+
scenario,
224222
).apply {
225223
inheritIO()
226224
}
227225

228226
runConformanceTest("client", scenario, processBuilder)
229227
}
230228

231-
private fun runConformanceTest(
232-
type: String,
233-
scenario: String,
234-
processBuilder: ProcessBuilder
235-
) {
229+
private fun runConformanceTest(type: String, scenario: String, processBuilder: ProcessBuilder) {
236230
logger.info { "Running $type conformance test: $scenario" }
237231

238232
val timeoutSeconds = System.getenv("CONFORMANCE_TEST_TIMEOUT_SECONDS")?.toLongOrNull()
@@ -242,16 +236,33 @@ class ConformanceTest {
242236
val completed = process.waitFor(timeoutSeconds, TimeUnit.SECONDS)
243237

244238
if (!completed) {
245-
logger.error { "${type.replaceFirstChar { it.uppercase() }} conformance test '$scenario' timed out after $timeoutSeconds seconds" }
239+
logger.error {
240+
"${type.replaceFirstChar {
241+
it.uppercase()
242+
}} conformance test '$scenario' timed out after $timeoutSeconds seconds"
243+
}
246244
process.destroyForcibly()
247-
throw AssertionError("${type.replaceFirstChar { it.uppercase() }} conformance test '$scenario' timed out after $timeoutSeconds seconds")
245+
throw AssertionError(
246+
"${type.replaceFirstChar {
247+
it.uppercase()
248+
}} conformance test '$scenario' timed out after $timeoutSeconds seconds",
249+
)
248250
}
249251

250252
when (val exitCode = process.exitValue()) {
251253
0 -> logger.info { "${type.replaceFirstChar { it.uppercase() }} conformance test '$scenario' passed!" }
254+
252255
else -> {
253-
logger.error { "${type.replaceFirstChar { it.uppercase() }} conformance test '$scenario' failed with exit code: $exitCode" }
254-
throw AssertionError("${type.replaceFirstChar { it.uppercase() }} conformance test '$scenario' failed (exit code: $exitCode). Check test output above for details.")
256+
logger.error {
257+
"${type.replaceFirstChar {
258+
it.uppercase()
259+
}} conformance test '$scenario' failed with exit code: $exitCode"
260+
}
261+
throw AssertionError(
262+
"${type.replaceFirstChar {
263+
it.uppercase()
264+
}} conformance test '$scenario' failed (exit code: $exitCode). Check test output above for details.",
265+
)
255266
}
256267
}
257268
}

0 commit comments

Comments
 (0)