diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a8a1825..9ad38f9b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ logging = "7.0.13" slf4j = "2.0.17" kotest = "6.0.4" awaitility = "4.3.0" -mokksy = "0.6.1" +mokksy = "0.6.2" [libraries] # Plugins diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt index 6f1e30fc..77ba553e 100644 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt @@ -12,7 +12,7 @@ import org.junit.jupiter.api.TestInstance internal abstract class AbstractStreamableHttpClientTest { // start mokksy on random port - protected val mockMcp: OldSchemaMockMcp = OldSchemaMockMcp(verbose = true) + protected val mockMcp: MockMcp = MockMcp(verbose = true) @AfterEach fun afterEach() { diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaMockMcp.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaMockMcp.kt deleted file mode 100644 index 5d1a1441..00000000 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaMockMcp.kt +++ /dev/null @@ -1,236 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.client - -import dev.mokksy.mokksy.BuildingStep -import dev.mokksy.mokksy.Mokksy -import dev.mokksy.mokksy.StubConfiguration -import io.ktor.http.ContentType -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.sse.ServerSentEvent -import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest -import io.modelcontextprotocol.kotlin.sdk.types.RequestId -import kotlinx.coroutines.flow.Flow -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.putJsonObject - -/** - * High-level helper for simulating an MCP server over Streaming HTTP transport with Server-Sent Events (SSE), - * built on top of an HTTP server using the [Mokksy](https://mokksy.dev) library. - * - * Provides test utilities to mock server behavior based on specific request conditions. - * - * @param verbose Whether to print detailed logs. Defaults to `false`. - * @author Konstantin Pavlov - */ -internal class OldSchemaMockMcp(verbose: Boolean = false) { - - private val mokksy: Mokksy = Mokksy(verbose = verbose) - - fun checkForUnmatchedRequests() { - mokksy.checkForUnmatchedRequests() - } - - val url = "${mokksy.baseUrl()}/mcp" - - @Suppress("LongParameterList") - fun onInitialize( - clientName: String? = null, - sessionId: String, - protocolVersion: String = "2025-03-26", - serverName: String = "Mock MCP Server", - serverVersion: String = "1.0.0", - capabilities: JsonObject = buildJsonObject { - putJsonObject("tools") { - put("listChanged", JsonPrimitive(false)) - } - }, - ) { - val predicates = if (clientName != null) { - arrayOf<(JSONRPCRequest?) -> Boolean>({ - it?.params?.jsonObject - ?.get("clientInfo")?.jsonObject - ?.get("name")?.jsonPrimitive - ?.contentOrNull == clientName - }) - } else { - emptyArray() - } - - handleWithResult( - jsonRpcMethod = "initialize", - sessionId = sessionId, - bodyPredicates = predicates, - // language=json - result = """ - { - "capabilities": $capabilities, - "protocolVersion": "$protocolVersion", - "serverInfo": { - "name": "$serverName", - "version": "$serverVersion" - }, - "_meta": { - "foo": "bar" - } - } - """.trimIndent(), - ) - } - - fun onJSONRPCRequest( - httpMethod: HttpMethod = HttpMethod.Post, - jsonRpcMethod: String, - expectedSessionId: String? = null, - vararg bodyPredicates: (JSONRPCRequest) -> Boolean, - ): BuildingStep = mokksy.method( - configuration = StubConfiguration(removeAfterMatch = true), - httpMethod = httpMethod, - requestType = JSONRPCRequest::class, - ) { - path("/mcp") - expectedSessionId?.let { - containsHeader(MCP_SESSION_ID_HEADER, it) - } - bodyMatchesPredicate( - description = "JSON-RPC version is '2.0'", - predicate = - { - it!!.jsonrpc == "2.0" - }, - ) - bodyMatchesPredicate( - description = "JSON-RPC Method should be '$jsonRpcMethod'", - predicate = - { - it!!.method == jsonRpcMethod - }, - ) - bodyPredicates.forEach { predicate -> - bodyMatchesPredicate(predicate = { predicate.invoke(it!!) }) - } - } - - @Suppress("LongParameterList") - fun handleWithResult( - httpMethod: HttpMethod = HttpMethod.Post, - jsonRpcMethod: String, - expectedSessionId: String? = null, - sessionId: String, - contentType: ContentType = ContentType.Application.Json, - statusCode: HttpStatusCode = HttpStatusCode.OK, - vararg bodyPredicates: (JSONRPCRequest) -> Boolean, - result: () -> JsonObject, - ) { - onJSONRPCRequest( - httpMethod = httpMethod, - jsonRpcMethod = jsonRpcMethod, - expectedSessionId = expectedSessionId, - bodyPredicates = bodyPredicates, - ) respondsWith { - val requestId = when (request.body.id) { - is RequestId.NumberId -> (request.body.id as RequestId.NumberId).value.toString() - is RequestId.StringId -> "\"${(request.body.id as RequestId.StringId).value}\"" - } - val resultObject = result!!.invoke() - // language=json - body = """ - { - "jsonrpc": "2.0", - "id": $requestId, - "result": $resultObject - } - """.trimIndent() - this.contentType = contentType - headers += MCP_SESSION_ID_HEADER to sessionId - httpStatus = statusCode - } - } - - @Suppress("LongParameterList") - fun handleWithResult( - httpMethod: HttpMethod = HttpMethod.Post, - jsonRpcMethod: String, - expectedSessionId: String? = null, - sessionId: String, - contentType: ContentType = ContentType.Application.Json, - statusCode: HttpStatusCode = HttpStatusCode.OK, - vararg bodyPredicates: (JSONRPCRequest) -> Boolean, - result: String, - ) { - handleWithResult( - httpMethod = httpMethod, - jsonRpcMethod = jsonRpcMethod, - expectedSessionId = expectedSessionId, - sessionId = sessionId, - contentType = contentType, - statusCode = statusCode, - bodyPredicates = bodyPredicates, - result = { - Json.parseToJsonElement(result).jsonObject - }, - ) - } - - @Suppress("LongParameterList") - fun handleJSONRPCRequest( - httpMethod: HttpMethod = HttpMethod.Post, - jsonRpcMethod: String, - expectedSessionId: String? = null, - sessionId: String, - contentType: ContentType = ContentType.Application.Json, - statusCode: HttpStatusCode = HttpStatusCode.OK, - vararg bodyPredicates: (JSONRPCRequest?) -> Boolean, - bodyBuilder: () -> String = { "" }, - ) { - onJSONRPCRequest( - httpMethod = httpMethod, - jsonRpcMethod = jsonRpcMethod, - expectedSessionId = expectedSessionId, - bodyPredicates = bodyPredicates, - ) respondsWith { - body = bodyBuilder.invoke() - this.contentType = contentType - headers += MCP_SESSION_ID_HEADER to sessionId - httpStatus = statusCode - } - } - - fun onSubscribe(httpMethod: HttpMethod = HttpMethod.Post, sessionId: String): BuildingStep = mokksy.method( - httpMethod = httpMethod, - name = "MCP GETs", - requestType = Any::class, - ) { - path("/mcp") - containsHeader(MCP_SESSION_ID_HEADER, sessionId) - containsHeader("Accept", "application/json,text/event-stream") - containsHeader("Cache-Control", "no-store") - } - - fun handleSubscribeWithGet(sessionId: String, block: () -> Flow) { - onSubscribe( - httpMethod = HttpMethod.Get, - sessionId = sessionId, - ) respondsWithSseStream { - headers += MCP_SESSION_ID_HEADER to sessionId - this.flow = block.invoke() - } - } - - fun mockUnsubscribeRequest(sessionId: String) { - mokksy.delete( - configuration = StubConfiguration(removeAfterMatch = true), - requestType = JSONRPCRequest::class, - ) { - path("/mcp") - containsHeader(MCP_SESSION_ID_HEADER, sessionId) - } respondsWith { - body = null - } - } -} diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt index 37f5a307..660aa240 100644 --- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt +++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt @@ -207,7 +207,7 @@ internal class StreamableHttpClientTest : AbstractStreamableHttpClientTest() { delay(1.seconds) - client.ping() // connection is still alive + client.ping() // the connection is still alive client.close() }