11package  io.modelcontextprotocol.kotlin.sdk.client 
22
3+ import  dev.mokksy.mokksy.BuildingStep 
34import  dev.mokksy.mokksy.Mokksy 
45import  dev.mokksy.mokksy.StubConfiguration 
56import  io.ktor.http.ContentType 
67import  io.ktor.http.HttpMethod 
78import  io.ktor.http.HttpStatusCode 
89import  io.ktor.sse.ServerSentEvent 
910import  io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest 
11+ import  io.modelcontextprotocol.kotlin.sdk.RequestId 
1012import  kotlinx.coroutines.flow.Flow 
13+ import  kotlinx.serialization.json.Json 
14+ import  kotlinx.serialization.json.JsonObject 
15+ import  kotlinx.serialization.json.JsonPrimitive 
16+ import  kotlinx.serialization.json.buildJsonObject 
17+ import  kotlinx.serialization.json.contentOrNull 
18+ import  kotlinx.serialization.json.jsonObject 
19+ import  kotlinx.serialization.json.jsonPrimitive 
20+ import  kotlinx.serialization.json.putJsonObject 
21+ 
22+ const  val  MCP_SESSION_ID_HEADER  =  " Mcp-Session-Id" 
1123
1224/* *
1325 * High-level helper for simulating an MCP server over Streaming HTTP transport with Server-Sent Events (SSE), 
@@ -26,51 +38,188 @@ internal class MockMcp(verbose: Boolean = false) {
2638        mokksy.checkForUnmatchedRequests()
2739    }
2840
29-     val  url =  mokksy.baseUrl()  +   " /mcp" 
41+     val  url =  " ${ mokksy.baseUrl()} " 
3042
3143    @Suppress(" LongParameterList" 
44+     fun  onInitialize (
45+         clientName :  String?  = null,
46+         sessionId :  String ,
47+         protocolVersion :  String  = "2025-03-26",
48+         serverName :  String  = "Mock  MCP  Server ",
49+         serverVersion :  String  = "1.0.0",
50+         capabilities :  JsonObject  = buildJsonObject {
51+             putJsonObject("tools") {
52+                 put("listChanged", JsonPrimitive (false))
53+             }
54+         },
55+     ) {
56+         val  predicates =  if  (clientName !=  null ) {
57+             arrayOf< (JSONRPCRequest ? ) ->  Boolean > ({
58+                 it?.params?.jsonObject
59+                     ?.get(" clientInfo" 
60+                     ?.get(" name" 
61+                     ?.contentOrNull ==  clientName
62+             })
63+         } else  {
64+             emptyArray()
65+         }
66+ 
67+         handleWithResult(
68+             jsonRpcMethod =  " initialize" 
69+             sessionId =  sessionId,
70+             bodyPredicates =  predicates,
71+             //  language=json
72+             result =  """ 
73+             { 
74+                 "capabilities": $capabilities , 
75+                 "protocolVersion": "$protocolVersion ", 
76+                 "serverInfo": { 
77+                   "name": "$serverName ", 
78+                   "version": "$serverVersion " 
79+                 }, 
80+                 "_meta": { 
81+                   "foo": "bar" 
82+                 } 
83+             } 
84+             """  .trimIndent(),
85+         )
86+     }
87+ 
3288    fun  onJSONRPCRequest (
89+         httpMethod :  HttpMethod  = HttpMethod .Post ,
90+         jsonRpcMethod :  String ,
91+         expectedSessionId :  String?  = null,
92+         vararg  bodyPredicates :  (JSONRPCRequest ) ->  Boolean ,
93+     ): BuildingStep <JSONRPCRequest > =  mokksy.method(
94+         configuration =  StubConfiguration (removeAfterMatch =  true ),
95+         httpMethod =  httpMethod,
96+         requestType =  JSONRPCRequest ::class ,
97+     ) {
98+         path(" /mcp" 
99+         expectedSessionId?.let  {
100+             containsHeader(MCP_SESSION_ID_HEADER , it)
101+         }
102+         bodyMatchesPredicate(
103+             description =  " JSON-RPC version is '2.0'" 
104+             predicate = 
105+             {
106+                 it!! .jsonrpc ==  " 2.0" 
107+             },
108+         )
109+         bodyMatchesPredicate(
110+             description =  " JSON-RPC Method should be '$jsonRpcMethod '" 
111+             predicate = 
112+             {
113+                 it!! .method ==  jsonRpcMethod
114+             },
115+         )
116+         bodyPredicates.forEach { predicate -> 
117+             bodyMatchesPredicate(predicate =  { predicate.invoke(it!! ) })
118+         }
119+     }
120+ 
121+     @Suppress(" LongParameterList" 
122+     fun  handleWithResult (
33123        httpMethod :  HttpMethod  = HttpMethod .Post ,
34124        jsonRpcMethod :  String ,
35125        expectedSessionId :  String?  = null,
36126        sessionId :  String ,
37127        contentType :  ContentType  = ContentType .Application .Json ,
38128        statusCode :  HttpStatusCode  = HttpStatusCode .OK ,
39-         bodyBuilder :  () ->  String ,
129+         vararg  bodyPredicates :  (JSONRPCRequest ) ->  Boolean ,
130+         result :  () ->  JsonObject ,
40131    ) {
41-         mokksy.method(
42-             configuration =  StubConfiguration (removeAfterMatch =  true ),
132+         onJSONRPCRequest(
43133            httpMethod =  httpMethod,
44-             requestType =  JSONRPCRequest ::class ,
45-         ) {
46-             path(" /mcp" 
47-             expectedSessionId?.let  {
48-                 containsHeader(" Mcp-Session-Id" 
134+             jsonRpcMethod =  jsonRpcMethod,
135+             expectedSessionId =  expectedSessionId,
136+             bodyPredicates =  bodyPredicates,
137+         ) respondsWith {
138+             val  requestId =  when  (request.body.id) {
139+                 is  RequestId .NumberId  ->  (request.body.id as  RequestId .NumberId ).value.toString()
140+                 is  RequestId .StringId  ->  " \" ${(request.body.id as  RequestId .StringId ).value} \" " 
49141            }
50-             bodyMatchesPredicates(
51-                 {
52-                     it!! .method ==  jsonRpcMethod
53-                 },
54-                 {
55-                     it!! .jsonrpc ==  " 2.0" 
56-                 },
57-             )
58-         } respondsWith {
142+             val  resultObject =  result!! .invoke()
143+             //  language=json
144+             body =  """ 
145+              { 
146+               "jsonrpc": "2.0", 
147+               "id": $requestId , 
148+               "result": $resultObject  
149+             } 
150+             """  .trimIndent()
151+             this .contentType =  contentType
152+             headers + =  MCP_SESSION_ID_HEADER  to sessionId
153+             httpStatus =  statusCode
154+         }
155+     }
156+ 
157+     @Suppress(" LongParameterList" 
158+     fun  handleWithResult (
159+         httpMethod :  HttpMethod  = HttpMethod .Post ,
160+         jsonRpcMethod :  String ,
161+         expectedSessionId :  String?  = null,
162+         sessionId :  String ,
163+         contentType :  ContentType  = ContentType .Application .Json ,
164+         statusCode :  HttpStatusCode  = HttpStatusCode .OK ,
165+         vararg  bodyPredicates :  (JSONRPCRequest ) ->  Boolean ,
166+         result :  String ,
167+     ) {
168+         handleWithResult(
169+             httpMethod =  httpMethod,
170+             jsonRpcMethod =  jsonRpcMethod,
171+             expectedSessionId =  expectedSessionId,
172+             sessionId =  sessionId,
173+             contentType =  contentType,
174+             statusCode =  statusCode,
175+             bodyPredicates =  bodyPredicates,
176+             result =  {
177+                 Json .parseToJsonElement(result).jsonObject
178+             },
179+         )
180+     }
181+ 
182+     @Suppress(" LongParameterList" 
183+     fun  handleJSONRPCRequest (
184+         httpMethod :  HttpMethod  = HttpMethod .Post ,
185+         jsonRpcMethod :  String ,
186+         expectedSessionId :  String?  = null,
187+         sessionId :  String ,
188+         contentType :  ContentType  = ContentType .Application .Json ,
189+         statusCode :  HttpStatusCode  = HttpStatusCode .OK ,
190+         vararg  bodyPredicates :  (JSONRPCRequest ? ) ->  Boolean ,
191+         bodyBuilder :  () ->  String  = { "" },
192+     ) {
193+         onJSONRPCRequest(
194+             httpMethod =  httpMethod,
195+             jsonRpcMethod =  jsonRpcMethod,
196+             expectedSessionId =  expectedSessionId,
197+             bodyPredicates =  bodyPredicates,
198+         ) respondsWith {
59199            body =  bodyBuilder.invoke()
60200            this .contentType =  contentType
61-             headers + =  " Mcp-Session-Id " 
201+             headers + =  MCP_SESSION_ID_HEADER  to sessionId
62202            httpStatus =  statusCode
63203        }
64204    }
65205
66-     fun  onSubscribeWithGet (sessionId :  String , block :  () ->  Flow <ServerSentEvent >) {
67-         mokksy.get(name =  " MCP GETs" =  Any ::class ) {
68-             path(" /mcp" 
69-             containsHeader(" Mcp-Session-Id" 
70-             containsHeader(" Accept" " application/json,text/event-stream" 
71-             containsHeader(" Cache-Control" " no-store" 
72-         } respondsWithSseStream {
73-             headers + =  " Mcp-Session-Id" 
206+     fun  onSubscribe (httpMethod :  HttpMethod  = HttpMethod .Post , sessionId :  String ): BuildingStep <Any > =  mokksy.method(
207+         httpMethod =  httpMethod,
208+         name =  " MCP GETs" 
209+         requestType =  Any ::class ,
210+     ) {
211+         path(" /mcp" 
212+         containsHeader(MCP_SESSION_ID_HEADER , sessionId)
213+         containsHeader(" Accept" " application/json,text/event-stream" 
214+         containsHeader(" Cache-Control" " no-store" 
215+     }
216+ 
217+     fun  handleSubscribeWithGet (sessionId :  String , block :  () ->  Flow <ServerSentEvent >) {
218+         onSubscribe(
219+             httpMethod =  HttpMethod .Get ,
220+             sessionId =  sessionId,
221+         ) respondsWithSseStream {
222+             headers + =  MCP_SESSION_ID_HEADER  to sessionId
74223            this .flow =  block.invoke()
75224        }
76225    }
@@ -81,7 +230,7 @@ internal class MockMcp(verbose: Boolean = false) {
81230            requestType =  JSONRPCRequest ::class ,
82231        ) {
83232            path(" /mcp" 
84-             containsHeader(" Mcp-Session-Id " 
233+             containsHeader(MCP_SESSION_ID_HEADER , sessionId)
85234        } respondsWith {
86235            body =  null 
87236        }
0 commit comments