Skip to content

Commit f964123

Browse files
committed
Add MCP conformance test coverage
1 parent 29d89ab commit f964123

File tree

6 files changed

+790
-0
lines changed

6 files changed

+790
-0
lines changed

.github/workflows/build.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ jobs:
2828
java-version: '21'
2929
distribution: 'temurin'
3030

31+
- name: Setup Node.js
32+
uses: actions/setup-node@v4
33+
with:
34+
node-version: '20'
35+
3136
- name: Setup Gradle
3237
uses: gradle/actions/setup-gradle@v5
3338
with:
@@ -57,6 +62,16 @@ jobs:
5762
working-directory: ./samples/weather-stdio-server
5863
run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT
5964

65+
- name: Run Conformance Tests
66+
run: ./gradlew conformance
67+
68+
- name: Upload Conformance Results
69+
if: always()
70+
uses: actions/upload-artifact@v5
71+
with:
72+
name: conformance-results
73+
path: kotlin-sdk-test/results/
74+
6075
- name: Upload Reports
6176
if: ${{ !cancelled() }}
6277
uses: actions/upload-artifact@v5

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,6 @@ dist
5656
### SWE agents ###
5757
.claude/
5858
.junie/
59+
60+
### Conformance test results ###
61+
kotlin-sdk-test/results/

kotlin-sdk-test/build.gradle.kts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
2+
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation
3+
14
plugins {
25
id("mcp.multiplatform")
36
}
@@ -31,3 +34,37 @@ kotlin {
3134
}
3235
}
3336
}
37+
38+
tasks.register<Test>("conformance") {
39+
group = "conformance"
40+
description = "Run MCP conformance tests with detailed output"
41+
42+
val jvmCompilation = kotlin.targets["jvm"].compilations["test"] as KotlinJvmCompilation
43+
testClassesDirs = jvmCompilation.output.classesDirs
44+
classpath = jvmCompilation.runtimeDependencyFiles
45+
46+
useJUnitPlatform()
47+
48+
filter {
49+
includeTestsMatching("*ConformanceTest*")
50+
}
51+
52+
testLogging {
53+
events("passed", "skipped", "failed")
54+
showStandardStreams = true
55+
showExceptions = true
56+
showCauses = true
57+
showStackTraces = true
58+
exceptionFormat = TestExceptionFormat.FULL
59+
}
60+
61+
doFirst {
62+
systemProperty("test.classpath", classpath.asPath)
63+
64+
println("\n" + "=".repeat(60))
65+
println("MCP CONFORMANCE TESTS")
66+
println("=".repeat(60))
67+
println("These tests validate compliance with the MCP specification.")
68+
println("=".repeat(60) + "\n")
69+
}
70+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package io.modelcontextprotocol.kotlin.sdk.conformance
2+
3+
import io.github.oshai.kotlinlogging.KotlinLogging
4+
import io.ktor.client.HttpClient
5+
import io.ktor.client.engine.cio.CIO
6+
import io.ktor.client.plugins.sse.SSE
7+
import io.modelcontextprotocol.kotlin.sdk.client.Client
8+
import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport
9+
import io.modelcontextprotocol.kotlin.sdk.shared.Transport
10+
import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest
11+
import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams
12+
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
13+
import kotlinx.coroutines.runBlocking
14+
import kotlinx.serialization.json.JsonPrimitive
15+
import kotlinx.serialization.json.buildJsonObject
16+
17+
private val logger = KotlinLogging.logger {}
18+
19+
fun main(args: Array<String>) {
20+
require(args.isNotEmpty()) {
21+
"Usage: ConformanceClient <server-url>"
22+
}
23+
24+
val serverUrl = args.last()
25+
logger.info { "Connecting to test server at: $serverUrl" }
26+
27+
val httpClient = HttpClient(CIO) {
28+
install(SSE)
29+
}
30+
val transport: Transport = StreamableHttpClientTransport(httpClient, serverUrl)
31+
32+
val client = Client(
33+
clientInfo = Implementation(
34+
name = "kotlin-conformance-client",
35+
version = "1.0.0"
36+
)
37+
)
38+
39+
var exitCode = 0
40+
41+
runBlocking {
42+
try {
43+
client.connect(transport)
44+
logger.info { "✅ Connected to server successfully" }
45+
46+
try {
47+
val tools = client.listTools()
48+
logger.info { "Available tools: ${tools.tools.map { it.name }}" }
49+
50+
val addNumbersTool = tools.tools.find { it.name == "add_numbers" }
51+
if (addNumbersTool != null) {
52+
logger.info { "Calling tool: add_numbers" }
53+
val result = client.callTool(
54+
CallToolRequest(
55+
params = CallToolRequestParams(
56+
name = "add_numbers",
57+
arguments = buildJsonObject {
58+
put("a", JsonPrimitive(5))
59+
put("b", JsonPrimitive(3))
60+
}
61+
)
62+
)
63+
)
64+
logger.info { "Tool result: ${result.content}" }
65+
} else if (tools.tools.isNotEmpty()) {
66+
val toolName = tools.tools.first().name
67+
logger.info { "Calling tool: $toolName" }
68+
69+
val result = client.callTool(
70+
CallToolRequest(
71+
params = CallToolRequestParams(
72+
name = toolName,
73+
arguments = buildJsonObject {
74+
put("input", JsonPrimitive("test"))
75+
}
76+
)
77+
)
78+
)
79+
logger.info { "Tool result: ${result.content}" }
80+
}
81+
} catch (e: Exception) {
82+
logger.debug(e) { "Error during tool operations (may be expected for some scenarios)" }
83+
}
84+
85+
logger.info { "✅ Client operations completed successfully" }
86+
} catch (e: Exception) {
87+
logger.error(e) { "❌ Client failed" }
88+
exitCode = 1
89+
} finally {
90+
try {
91+
transport.close()
92+
} catch (e: Exception) {
93+
logger.warn(e) { "Error closing transport" }
94+
}
95+
httpClient.close()
96+
}
97+
}
98+
99+
kotlin.system.exitProcess(exitCode)
100+
}

0 commit comments

Comments
 (0)