4
4
5
5
package io .modelcontextprotocol .server .transport ;
6
6
7
- import java .io .BufferedReader ;
8
- import java .io .IOException ;
9
- import java .io .PrintWriter ;
10
-
11
- import org .slf4j .Logger ;
12
- import org .slf4j .LoggerFactory ;
13
-
14
7
import com .fasterxml .jackson .databind .ObjectMapper ;
15
-
16
- import io .modelcontextprotocol .server .DefaultMcpTransportContext ;
17
8
import io .modelcontextprotocol .server .McpStatelessServerHandler ;
18
9
import io .modelcontextprotocol .server .McpTransportContext ;
19
10
import io .modelcontextprotocol .server .McpTransportContextExtractor ;
11
+ import io .modelcontextprotocol .server .StatelessMcpTransportContext ;
20
12
import io .modelcontextprotocol .spec .McpError ;
21
13
import io .modelcontextprotocol .spec .McpSchema ;
22
14
import io .modelcontextprotocol .spec .McpStatelessServerTransport ;
26
18
import jakarta .servlet .http .HttpServlet ;
27
19
import jakarta .servlet .http .HttpServletRequest ;
28
20
import jakarta .servlet .http .HttpServletResponse ;
21
+ import org .slf4j .Logger ;
22
+ import org .slf4j .LoggerFactory ;
29
23
import reactor .core .publisher .Mono ;
30
24
25
+ import java .io .BufferedReader ;
26
+ import java .io .IOException ;
27
+ import java .io .PrintWriter ;
28
+ import java .util .concurrent .atomic .AtomicBoolean ;
29
+ import java .util .concurrent .atomic .AtomicInteger ;
30
+ import java .util .function .BiConsumer ;
31
+
31
32
/**
32
33
* Implementation of an HttpServlet based {@link McpStatelessServerTransport}.
33
34
*
@@ -123,7 +124,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
123
124
return ;
124
125
}
125
126
126
- McpTransportContext transportContext = this .contextExtractor .extract (request , new DefaultMcpTransportContext ());
127
+ AtomicInteger nextId = new AtomicInteger (0 );
128
+ AtomicBoolean upgradedToSse = new AtomicBoolean (false );
129
+ BiConsumer <String , Object > notificationHandler = buildNotificationHandler (response , upgradedToSse , nextId );
130
+ McpTransportContext transportContext = this .contextExtractor .extract (request ,
131
+ new StatelessMcpTransportContext (notificationHandler ));
127
132
128
133
String accept = request .getHeader (ACCEPT );
129
134
if (accept == null || !(accept .contains (APPLICATION_JSON ) && accept .contains (TEXT_EVENT_STREAM ))) {
@@ -149,14 +154,19 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
149
154
.contextWrite (ctx -> ctx .put (McpTransportContext .KEY , transportContext ))
150
155
.block ();
151
156
152
- response .setContentType (APPLICATION_JSON );
153
- response .setCharacterEncoding (UTF_8 );
154
- response .setStatus (HttpServletResponse .SC_OK );
155
-
156
157
String jsonResponseText = objectMapper .writeValueAsString (jsonrpcResponse );
157
- PrintWriter writer = response .getWriter ();
158
- writer .write (jsonResponseText );
159
- writer .flush ();
158
+ if (upgradedToSse .get ()) {
159
+ sendEvent (response .getWriter (), jsonResponseText , nextId .getAndIncrement ());
160
+ }
161
+ else {
162
+ response .setContentType (APPLICATION_JSON );
163
+ response .setCharacterEncoding (UTF_8 );
164
+ response .setStatus (HttpServletResponse .SC_OK );
165
+
166
+ PrintWriter writer = response .getWriter ();
167
+ writer .write (jsonResponseText );
168
+ writer .flush ();
169
+ }
160
170
}
161
171
catch (Exception e ) {
162
172
logger .error ("Failed to handle request: {}" , e .getMessage ());
@@ -303,4 +313,43 @@ public HttpServletStatelessServerTransport build() {
303
313
304
314
}
305
315
316
+ private BiConsumer <String , Object > buildNotificationHandler (HttpServletResponse response ,
317
+ AtomicBoolean upgradedToSse , AtomicInteger nextId ) {
318
+ AtomicBoolean responseInitialized = new AtomicBoolean (false );
319
+
320
+ return (notificationMethod , params ) -> {
321
+ upgradedToSse .set (true );
322
+
323
+ if (responseInitialized .compareAndSet (false , true )) {
324
+ response .setContentType (TEXT_EVENT_STREAM );
325
+ response .setCharacterEncoding (UTF_8 );
326
+ response .setStatus (HttpServletResponse .SC_OK );
327
+ }
328
+
329
+ McpSchema .JSONRPCNotification notification = new McpSchema .JSONRPCNotification (McpSchema .JSONRPC_VERSION ,
330
+ notificationMethod , params );
331
+ try {
332
+ sendEvent (response .getWriter (), objectMapper .writeValueAsString (notification ),
333
+ nextId .getAndIncrement ());
334
+ }
335
+ catch (IOException e ) {
336
+ logger .error ("Failed to handle notification: {}" , e .getMessage ());
337
+ throw new McpError (new McpSchema .JSONRPCResponse .JSONRPCError (McpSchema .ErrorCodes .INTERNAL_ERROR ,
338
+ e .getMessage (), null ));
339
+ }
340
+ };
341
+ }
342
+
343
+ private void sendEvent (PrintWriter writer , String data , int id ) throws IOException {
344
+ // tested with MCP inspector. Event must consist of these two fields and only
345
+ // these two fields
346
+ writer .write ("id: " + id + "\n " );
347
+ writer .write ("data: " + data + "\n \n " );
348
+ writer .flush ();
349
+
350
+ if (writer .checkError ()) {
351
+ throw new IOException ("Client disconnected" );
352
+ }
353
+ }
354
+
306
355
}
0 commit comments