@@ -8,6 +8,8 @@ import io.ktor.client.engine.apache5.Apache5
88import io.ktor.client.plugins.logging.LogLevel
99import io.ktor.client.plugins.logging.Logging
1010import io.ktor.client.plugins.sse.SSE
11+ import io.ktor.http.ContentType
12+ import io.ktor.http.HttpMethod
1113import io.ktor.http.HttpStatusCode
1214import io.ktor.sse.ServerSentEvent
1315import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities
@@ -27,6 +29,12 @@ import kotlin.test.AfterTest
2729import kotlin.test.Test
2830import kotlin.time.Duration.Companion.milliseconds
2931
32+ /* *
33+ * Integration tests for the `StreamableHttpClientTransport` implementation
34+ * using the [Mokksy](https://mokksy.dev) library
35+ * to simulate Streaming HTTP with server-sent events (SSE).
36+ * @author Konstantin Pavlov
37+ */
3038@TestInstance(TestInstance .Lifecycle .PER_CLASS )
3139class StreamableHttpClientTest {
3240
@@ -43,6 +51,7 @@ class StreamableHttpClientTest {
4351 }
4452
4553 @Test
54+ @Suppress(" LongMethod" )
4655 fun `test streamableHttpClient` (): Unit = runBlocking {
4756 val client = Client (
4857 clientInfo = Implementation (name = " sample-client" , version = " 1.0.0" ),
@@ -53,7 +62,7 @@ class StreamableHttpClientTest {
5362
5463 val sessionId = UUID .randomUUID().toString()
5564
56- mockPostRequest (
65+ mockRequest (
5766 method = " initialize" ,
5867 sessionId = sessionId,
5968 ) {
@@ -81,7 +90,7 @@ class StreamableHttpClientTest {
8190 """ .trimIndent()
8291 }
8392
84- mockPostRequest (
93+ mockRequest (
8594 method = " notifications/initialized" ,
8695 sessionId = sessionId,
8796 statusCode = HttpStatusCode .Accepted ,
@@ -103,15 +112,15 @@ class StreamableHttpClientTest {
103112 ServerSentEvent (
104113 event = " message" ,
105114 id = " 1" ,
106- data = @Suppress(" ktlint:standard:max-line-length " )
115+ data = @Suppress(" MaxLineLength " )
107116 // language=json
108117 """ {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""" ,
109118 ),
110119 )
111120 delay(200 .milliseconds)
112121 emit(
113122 ServerSentEvent (
114- data = @Suppress(" ktlint:standard:max-line-length " )
123+ data = @Suppress(" MaxLineLength " )
115124 // language=json
116125 """ {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""" ,
117126 ),
@@ -121,7 +130,7 @@ class StreamableHttpClientTest {
121130
122131 client.connect(
123132 StreamableHttpClientTransport (
124- url = " http://localhost: ${ mokksy.port()} /mcp" ,
133+ url = mokksy.baseUrl() + " /mcp" ,
125134 client = HttpClient (Apache5 ) {
126135 install(SSE )
127136 install(Logging ) {
@@ -133,45 +142,45 @@ class StreamableHttpClientTest {
133142
134143 // TODO: get notifications
135144
136- mockPostRequest (
145+ mockRequest (
137146 method = " tools/list" ,
138147 sessionId = sessionId,
139148 ) {
140149 // language=json
141150 """
142- {
143- "jsonrpc": "2.0",
144- "id": 3,
145- "result": {
146- "tools": [
147- {
148- "name": "get_weather",
149- "title": "Weather Information Provider",
150- "description": "Get current weather information for a location",
151- "inputSchema": {
152- "type": "object",
153- "properties": {
154- "location": {
155- "type": "string",
156- "description": "City name or zip code"
157- }
158- },
159- "required": ["location"]
160- },
161- "outputSchema": {
162- "type": "object",
163- "properties": {
164- "temperature": {
165- "type": "number",
166- "description": "Temperature, Celsius"
167- }
168- },
169- "required": ["temperature"]
170- }
171- }
172- ]
173- }
174- }
151+ {
152+ "jsonrpc": "2.0",
153+ "id": 3,
154+ "result": {
155+ "tools": [
156+ {
157+ "name": "get_weather",
158+ "title": "Weather Information Provider",
159+ "description": "Get current weather information for a location",
160+ "inputSchema": {
161+ "type": "object",
162+ "properties": {
163+ "location": {
164+ "type": "string",
165+ "description": "City name or zip code"
166+ }
167+ },
168+ "required": ["location"]
169+ },
170+ "outputSchema": {
171+ "type": "object",
172+ "properties": {
173+ "temperature": {
174+ "type": "number",
175+ "description": "Temperature, Celsius"
176+ }
177+ },
178+ "required": ["temperature"]
179+ }
180+ }
181+ ]
182+ }
183+ }
175184 """ .trimIndent()
176185 }
177186
@@ -203,16 +212,34 @@ class StreamableHttpClientTest {
203212 ),
204213 annotations = null ,
205214 )
215+
216+ mockDisconnect(sessionId = sessionId)
217+ }
218+
219+ fun mockDisconnect (sessionId : String ) {
220+ mokksy.delete(
221+ configuration = StubConfiguration (removeAfterMatch = true ),
222+ requestType = JSONRPCRequest ::class ,
223+ ) {
224+ path(" /mcp" )
225+ containsHeader(" Mcp-Session-Id" , sessionId)
226+ } respondsWith {
227+ body = null
228+ }
206229 }
207230
208- private fun mockPostRequest (
231+ @Suppress(" LongParameterList" )
232+ private fun mockRequest (
233+ httpMethod : HttpMethod = HttpMethod .Post ,
209234 method : String ,
210235 sessionId : String ,
236+ contentType : ContentType = ContentType .Application .Json ,
211237 statusCode : HttpStatusCode = HttpStatusCode .OK ,
212238 bodyBuilder : () -> String ,
213239 ) {
214- mokksy.post (
240+ mokksy.method (
215241 configuration = StubConfiguration (removeAfterMatch = true ),
242+ httpMethod = httpMethod,
216243 requestType = JSONRPCRequest ::class ,
217244 ) {
218245 path(" /mcp" )
@@ -226,7 +253,7 @@ class StreamableHttpClientTest {
226253 )
227254 } respondsWith {
228255 body = bodyBuilder.invoke()
229- headers + = " Content-Type " to " application/json; charset=utf-8 "
256+ this .contentType = contentType
230257 headers + = " Mcp-Session-Id" to sessionId
231258 httpStatus = statusCode
232259 }
0 commit comments