Skip to content

Commit f04ccfd

Browse files
skarpovdevkpavlov
andauthored
Add MCP conformance test coverage (#435)
<!-- Provide a brief summary of your changes --> ## Motivation and Context 1. The currently failing checks are commented out in `ConformanceTest`, see the TODOs. 2. I added detailed logging for Gradle task, Server, and Client to allow easier output reading when viewing tests results outside of IDE. 3. As a future improvement, instead of hard-coding scenarios, we can get them with `npx @modelcontextprotocol/conformance list` and run all of them. 4. The conformance framework writes its result into `kotlin-sdk-test/results` directory and contains JSONs like the one below: ```json [ { "id": "mcp-client-initialization", "name": "MCPClientInitialization", "description": "Validates that MCP client properly initializes with server", "status": "SUCCESS", "timestamp": "2025-11-28T14:38:36.718Z", "specReferences": [ { "id": "MCP-Lifecycle", "url": "https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle" } ], "details": { "protocolVersionSent": "2025-06-18", "expectedSpecVersion": "2025-06-18", "versionMatch": true, "clientName": "kotlin-conformance-client", "clientVersion": "1.0.0" } }, { "id": "server-info", "name": "ServerInfo", "description": "Test server info returned to client", "status": "INFO", "timestamp": "2025-11-28T14:38:36.718Z", "specReferences": [ { "id": "MCP-Lifecycle", "url": "https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle" } ], "details": { "serverName": "test-server", "serverVersion": "1.0.0" } } ] ``` As another future improvement, we can do somethig with these JSON files, like putting them into a job summary on GitHub. 5. The client test also generated stderr.txt file in `kotlin-sdk-test/results/initialize-.......` which contains errors because it tries to call a tool. I haven't investigated if it's just the way I wrote the client or related to `StreamableHttpClientTransport`. 6. The server is the simplified version of the existing server for Kotlin<--->Typescript tests. If conformance tests extend significantly, we can either combine the server implementations or ditch the integration tests. ## How Has This Been Tested? <!-- Have you tested this in a real application? Which scenarios were tested? --> ## Breaking Changes <!-- Will users need to update their code or configurations? --> ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions --> --------- Co-authored-by: Konstantin Pavlov <[email protected]>
1 parent 29d89ab commit f04ccfd

File tree

10 files changed

+1033
-1
lines changed

10 files changed

+1033
-1
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ jobs:
4141
clean \
4242
ktlintCheck \
4343
build \
44+
-x :conformance-test:test \
4445
koverLog koverHtmlReport \
4546
publishToMavenLocal \
4647
-Pversion=0.0.1-SNAPSHOT

.github/workflows/conformance.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Conformance Tests
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
branches: [ main ]
7+
push:
8+
branches: [ main ]
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
12+
# Cancel only when the run is NOT on `main` branch
13+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
14+
15+
jobs:
16+
run-conformance:
17+
runs-on: macos-latest-xlarge
18+
name: Run Conformance Tests
19+
timeout-minutes: 20
20+
env:
21+
JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g"
22+
steps:
23+
- uses: actions/checkout@v6
24+
25+
- name: Set up JDK 21
26+
uses: actions/setup-java@v5
27+
with:
28+
java-version: '21'
29+
distribution: 'temurin'
30+
31+
- name: Setup Node.js
32+
uses: actions/setup-node@v4
33+
with:
34+
node-version: '24.11.1'
35+
36+
- name: Setup Gradle
37+
uses: gradle/actions/setup-gradle@v5
38+
with:
39+
add-job-summary: 'always'
40+
cache-read-only: true
41+
42+
- name: Run Conformance Tests
43+
run: ./gradlew --no-daemon :conformance-test:test
44+
45+
- name: Upload Conformance Results
46+
if: ${{ !cancelled() }}
47+
uses: actions/upload-artifact@v5
48+
with:
49+
name: conformance-results
50+
path: conformance-test/results/

.github/workflows/gradle-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
uses: gradle/actions/setup-gradle@v5
3535

3636
- name: Clean Build with Gradle
37-
run: ./gradlew clean build
37+
run: ./gradlew clean build -x :conformance-test:test
3838

3939
- name: Publish to Maven Central Portal
4040
id: publish

.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+
conformance-test/results/

conformance-test/build.gradle.kts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
2+
3+
plugins {
4+
kotlin("jvm")
5+
}
6+
7+
dependencies {
8+
testImplementation(project(":kotlin-sdk"))
9+
testImplementation(kotlin("test"))
10+
testImplementation(libs.kotlin.logging)
11+
testImplementation(libs.ktor.client.cio)
12+
testImplementation(libs.ktor.server.cio)
13+
testImplementation(libs.ktor.server.websockets)
14+
testRuntimeOnly(libs.slf4j.simple)
15+
}
16+
17+
tasks.test {
18+
useJUnitPlatform()
19+
20+
testLogging {
21+
events("passed", "skipped", "failed")
22+
showStandardStreams = true
23+
showExceptions = true
24+
showCauses = true
25+
showStackTraces = true
26+
exceptionFormat = TestExceptionFormat.FULL
27+
}
28+
29+
doFirst {
30+
systemProperty("test.classpath", classpath.asPath)
31+
32+
println("\n" + "=".repeat(60))
33+
println("MCP CONFORMANCE TESTS")
34+
println("=".repeat(60))
35+
println("These tests validate compliance with the MCP specification.")
36+
println("=".repeat(60) + "\n")
37+
}
38+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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+
"Server URL must be provided as an argument"
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+
if (tools.tools.isNotEmpty()) {
51+
val toolName = tools.tools.first().name
52+
logger.info { "Calling tool: $toolName" }
53+
54+
val result = client.callTool(
55+
CallToolRequest(
56+
params = CallToolRequestParams(
57+
name = toolName,
58+
arguments = buildJsonObject {
59+
put("input", JsonPrimitive("test"))
60+
},
61+
),
62+
),
63+
)
64+
logger.info { "Tool result: ${result.content}" }
65+
}
66+
} catch (e: Exception) {
67+
logger.debug(e) { "Error during tool operations (may be expected for some scenarios)" }
68+
}
69+
70+
logger.info { "✅ Client operations completed successfully" }
71+
} catch (e: Exception) {
72+
logger.error(e) { "❌ Client failed" }
73+
exitCode = 1
74+
} finally {
75+
try {
76+
transport.close()
77+
} catch (e: Exception) {
78+
logger.warn(e) { "Error closing transport" }
79+
}
80+
httpClient.close()
81+
}
82+
}
83+
84+
kotlin.system.exitProcess(exitCode)
85+
}

0 commit comments

Comments
 (0)