17
17
import com .fasterxml .jackson .annotation .JsonTypeInfo ;
18
18
import com .fasterxml .jackson .annotation .JsonTypeInfo .As ;
19
19
import com .fasterxml .jackson .core .type .TypeReference ;
20
+ import com .fasterxml .jackson .databind .JsonNode ;
20
21
import com .fasterxml .jackson .databind .ObjectMapper ;
21
22
import io .modelcontextprotocol .util .Assert ;
22
23
import org .slf4j .Logger ;
29
30
* Context Protocol Schema</a>.
30
31
*
31
32
* @author Christian Tzolov
33
+ * @author Jihoon Kim
32
34
*/
33
35
public final class McpSchema {
34
36
@@ -140,42 +142,78 @@ public sealed interface Request
140
142
};
141
143
142
144
/**
143
- * Deserializes a JSON string into a JSONRPCMessage object.
145
+ * Deserializes a JSON string into a JSONRPCMessage object. Handles both single and
146
+ * batch JSON-RPC messages.
144
147
* @param objectMapper The ObjectMapper instance to use for deserialization
145
148
* @param jsonText The JSON string to deserialize
146
- * @return A JSONRPCMessage instance using either the {@link JSONRPCRequest},
147
- * {@link JSONRPCNotification}, or {@link JSONRPCResponse} classes.
149
+ * @return A JSONRPCMessage instance, either a {@link JSONRPCRequest},
150
+ * {@link JSONRPCNotification}, {@link JSONRPCResponse}, or
151
+ * {@link JSONRPCBatchRequest}, or {@link JSONRPCBatchResponse} based on the JSON
152
+ * structure.
148
153
* @throws IOException If there's an error during deserialization
149
154
* @throws IllegalArgumentException If the JSON structure doesn't match any known
150
155
* message type
151
156
*/
152
157
public static JSONRPCMessage deserializeJsonRpcMessage (ObjectMapper objectMapper , String jsonText )
153
158
throws IOException {
154
-
155
159
logger .debug ("Received JSON message: {}" , jsonText );
156
160
157
- var map = objectMapper .readValue (jsonText , MAP_TYPE_REF );
161
+ JsonNode rootNode = objectMapper .readTree (jsonText );
162
+
163
+ // Check if it's a batch request/response
164
+ if (rootNode .isArray ()) {
165
+ // Batch processing
166
+ List <JSONRPCMessage > messages = new ArrayList <>();
167
+ for (JsonNode node : rootNode ) {
168
+ Map <String , Object > map = objectMapper .convertValue (node , MAP_TYPE_REF );
169
+ messages .add (convertToJsonRpcMessage (map , objectMapper ));
170
+ }
158
171
159
- // Determine message type based on specific JSON structure
172
+ // If it's a batch response, return JSONRPCBatchResponse
173
+ if (messages .get (0 ) instanceof JSONRPCResponse ) {
174
+ return new JSONRPCBatchResponse (messages );
175
+ }
176
+ // If it's a batch request, return JSONRPCBatchRequest
177
+ else {
178
+ return new JSONRPCBatchRequest (messages );
179
+ }
180
+ }
181
+
182
+ // Single message processing
183
+ Map <String , Object > map = objectMapper .readValue (jsonText , MAP_TYPE_REF );
184
+ return convertToJsonRpcMessage (map , objectMapper );
185
+ }
186
+
187
+ /**
188
+ * Converts a map into a specific JSON-RPC message type. Based on the map's structure,
189
+ * this method determines whether the message is a {@link JSONRPCRequest},
190
+ * {@link JSONRPCNotification}, or {@link JSONRPCResponse}.
191
+ * @param map The map representing the JSON structure
192
+ * @param objectMapper The ObjectMapper instance to use for deserialization
193
+ * @return The corresponding JSONRPCMessage instance (could be {@link JSONRPCRequest},
194
+ * {@link JSONRPCNotification}, or {@link JSONRPCResponse})
195
+ * @throws IllegalArgumentException If the map structure doesn't match any known
196
+ * message type
197
+ */
198
+ private static JSONRPCMessage convertToJsonRpcMessage (Map <String , Object > map , ObjectMapper objectMapper ) {
160
199
if (map .containsKey ("method" ) && map .containsKey ("id" )) {
161
200
return objectMapper .convertValue (map , JSONRPCRequest .class );
162
201
}
163
- else if (map .containsKey ("method" ) && ! map . containsKey ( "id" ) ) {
202
+ else if (map .containsKey ("method" )) {
164
203
return objectMapper .convertValue (map , JSONRPCNotification .class );
165
204
}
166
205
else if (map .containsKey ("result" ) || map .containsKey ("error" )) {
167
206
return objectMapper .convertValue (map , JSONRPCResponse .class );
168
207
}
169
208
170
- throw new IllegalArgumentException ("Cannot deserialize JSONRPCMessage : " + jsonText );
209
+ throw new IllegalArgumentException ("Unknown JSON-RPC message type : " + map );
171
210
}
172
211
173
212
// ---------------------------
174
213
// JSON-RPC Message Types
175
214
// ---------------------------
176
- public sealed interface JSONRPCMessage permits JSONRPCRequest , JSONRPCNotification , JSONRPCResponse {
177
-
178
- String jsonrpc ();
215
+ public sealed interface JSONRPCMessage
216
+ permits JSONRPCRequest , JSONRPCBatchRequest , JSONRPCNotification , JSONRPCResponse , JSONRPCBatchResponse {
179
217
180
218
}
181
219
@@ -188,6 +226,26 @@ public record JSONRPCRequest( // @formatter:off
188
226
@ JsonProperty ("params" ) Object params ) implements JSONRPCMessage {
189
227
} // @formatter:on
190
228
229
+ public record JSONRPCBatchRequest (List <JSONRPCMessage > messages ) implements JSONRPCMessage {
230
+ public JSONRPCBatchRequest {
231
+ boolean valid = messages .stream ()
232
+ .allMatch (message -> message instanceof JSONRPCRequest || message instanceof JSONRPCNotification );
233
+ if (!valid ) {
234
+ throw new IllegalArgumentException (
235
+ "Only JSONRPCRequest or JSONRPCNotification are allowed in batch request." );
236
+ }
237
+ }
238
+ }
239
+
240
+ public record JSONRPCBatchResponse (List <JSONRPCMessage > responses ) implements JSONRPCMessage {
241
+ public JSONRPCBatchResponse {
242
+ boolean valid = responses .stream ().allMatch (response -> response instanceof JSONRPCResponse );
243
+ if (!valid ) {
244
+ throw new IllegalArgumentException ("Only JSONRPCResponse are allowed in batch response." );
245
+ }
246
+ }
247
+ }
248
+
191
249
@ JsonInclude (JsonInclude .Include .NON_ABSENT )
192
250
@ JsonIgnoreProperties (ignoreUnknown = true )
193
251
public record JSONRPCNotification ( // @formatter:off
0 commit comments