1717import com .fasterxml .jackson .annotation .JsonTypeInfo ;
1818import com .fasterxml .jackson .annotation .JsonTypeInfo .As ;
1919import com .fasterxml .jackson .core .type .TypeReference ;
20+ import com .fasterxml .jackson .databind .JsonNode ;
2021import com .fasterxml .jackson .databind .ObjectMapper ;
2122import io .modelcontextprotocol .util .Assert ;
2223import org .slf4j .Logger ;
2930 * Context Protocol Schema</a>.
3031 *
3132 * @author Christian Tzolov
33+ * @author Jihoon Kim
3234 */
3335public final class McpSchema {
3436
@@ -37,7 +39,7 @@ public final class McpSchema {
3739 private McpSchema () {
3840 }
3941
40- public static final String LATEST_PROTOCOL_VERSION = "2024-11-05 " ;
42+ public static final String LATEST_PROTOCOL_VERSION = "2025-03-26 " ;
4143
4244 public static final String JSONRPC_VERSION = "2.0" ;
4345
@@ -140,42 +142,78 @@ public sealed interface Request
140142 };
141143
142144 /**
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.
144147 * @param objectMapper The ObjectMapper instance to use for deserialization
145148 * @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.
148153 * @throws IOException If there's an error during deserialization
149154 * @throws IllegalArgumentException If the JSON structure doesn't match any known
150155 * message type
151156 */
152157 public static JSONRPCMessage deserializeJsonRpcMessage (ObjectMapper objectMapper , String jsonText )
153158 throws IOException {
154-
155159 logger .debug ("Received JSON message: {}" , jsonText );
156160
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+ }
158171
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 ) {
160199 if (map .containsKey ("method" ) && map .containsKey ("id" )) {
161200 return objectMapper .convertValue (map , JSONRPCRequest .class );
162201 }
163- else if (map .containsKey ("method" ) && ! map . containsKey ( "id" ) ) {
202+ else if (map .containsKey ("method" )) {
164203 return objectMapper .convertValue (map , JSONRPCNotification .class );
165204 }
166205 else if (map .containsKey ("result" ) || map .containsKey ("error" )) {
167206 return objectMapper .convertValue (map , JSONRPCResponse .class );
168207 }
169208
170- throw new IllegalArgumentException ("Cannot deserialize JSONRPCMessage : " + jsonText );
209+ throw new IllegalArgumentException ("Unknown JSON-RPC message type : " + map );
171210 }
172211
173212 // ---------------------------
174213 // JSON-RPC Message Types
175214 // ---------------------------
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 {
179217
180218 }
181219
@@ -188,6 +226,26 @@ public record JSONRPCRequest( // @formatter:off
188226 @ JsonProperty ("params" ) Object params ) implements JSONRPCMessage {
189227 } // @formatter:on
190228
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+
191249 @ JsonInclude (JsonInclude .Include .NON_ABSENT )
192250 @ JsonIgnoreProperties (ignoreUnknown = true )
193251 public record JSONRPCNotification ( // @formatter:off
0 commit comments