Skip to content

Commit 86b9151

Browse files
committed
Accept missing required fields on deserialization
Some clients or servers fail to provide required JSON fields so instead of failing we replace them by default values and log with WARN level and allow the application to make progress. Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
1 parent b1212fe commit 86b9151

2 files changed

Lines changed: 200 additions & 0 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,6 +1599,10 @@ public CallToolRequest build() {
15991599
* @param structuredContent An optional JSON object that represents the structured
16001600
* result of the tool call.
16011601
* @param meta See specification for notes on _meta usage
1602+
* <p>
1603+
* Note: {@code content} is required by the MCP specification. Deserialization accepts
1604+
* a missing value and substitutes an empty list to avoid breaking existing
1605+
* integrations that may omit the field.
16021606
*/
16031607
@JsonInclude(JsonInclude.Include.NON_ABSENT)
16041608
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -1612,6 +1616,17 @@ public record CallToolResult( // @formatter:off
16121616
Assert.notNull(content, "content must not be null");
16131617
}
16141618

1619+
@JsonCreator
1620+
static CallToolResult fromJson(@JsonProperty("content") List<Content> content,
1621+
@JsonProperty("isError") Boolean isError, @JsonProperty("structuredContent") Object structuredContent,
1622+
@JsonProperty("_meta") Map<String, Object> meta) {
1623+
if (content == null) {
1624+
logger.warn("CallToolResult: missing required fields during deserialization: content -> []");
1625+
content = List.of();
1626+
}
1627+
return new CallToolResult(content, isError, structuredContent, meta);
1628+
}
1629+
16151630
/**
16161631
* Creates a builder for {@link CallToolResult}.
16171632
* @return a new builder instance
@@ -1839,6 +1854,10 @@ public static ModelHint of(String name) {
18391854
*
18401855
* @param role The sender or recipient of messages and data in a conversation
18411856
* @param content The content of the message
1857+
* <p>
1858+
* Note: {@code role} and {@code content} are required by the MCP specification.
1859+
* Deserialization accepts missing values and substitutes defaults to avoid breaking
1860+
* existing integrations that may omit these fields.
18421861
*/
18431862
@JsonInclude(JsonInclude.Include.NON_ABSENT)
18441863
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -1850,6 +1869,24 @@ public record SamplingMessage( // @formatter:off
18501869
Assert.notNull(role, "role must not be null");
18511870
Assert.notNull(content, "content must not be null");
18521871
}
1872+
1873+
@JsonCreator
1874+
static SamplingMessage fromJson(@JsonProperty("role") Role role, @JsonProperty("content") Content content) {
1875+
if (role == null || content == null) {
1876+
List<String> missing = new ArrayList<>();
1877+
if (role == null) {
1878+
missing.add("role -> 'user'");
1879+
role = Role.USER;
1880+
}
1881+
if (content == null) {
1882+
missing.add("content -> ''");
1883+
content = new TextContent("");
1884+
}
1885+
logger.warn("SamplingMessage: missing required fields during deserialization: {}",
1886+
String.join(", ", missing));
1887+
}
1888+
return new SamplingMessage(role, content);
1889+
}
18531890
}
18541891

18551892
/**
@@ -1873,6 +1910,10 @@ public record SamplingMessage( // @formatter:off
18731910
* @param metadata Optional metadata to pass through to the LLM provider. The format
18741911
* of this metadata is provider-specific
18751912
* @param meta See specification for notes on _meta usage
1913+
* <p>
1914+
* Note: {@code messages} and {@code maxTokens} are required by the MCP specification.
1915+
* Deserialization accepts missing values and substitutes defaults to avoid breaking
1916+
* existing integrations that may omit these fields.
18761917
*/
18771918
@JsonInclude(JsonInclude.Include.NON_ABSENT)
18781919
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -1892,6 +1933,32 @@ public record CreateMessageRequest( // @formatter:off
18921933
Assert.notNull(maxTokens, "maxTokens must not be null");
18931934
}
18941935

1936+
@JsonCreator
1937+
static CreateMessageRequest fromJson(@JsonProperty("messages") List<SamplingMessage> messages,
1938+
@JsonProperty("modelPreferences") ModelPreferences modelPreferences,
1939+
@JsonProperty("systemPrompt") String systemPrompt,
1940+
@JsonProperty("includeContext") ContextInclusionStrategy includeContext,
1941+
@JsonProperty("temperature") Double temperature, @JsonProperty("maxTokens") Integer maxTokens,
1942+
@JsonProperty("stopSequences") List<String> stopSequences,
1943+
@JsonProperty("metadata") Map<String, Object> metadata,
1944+
@JsonProperty("_meta") Map<String, Object> meta) {
1945+
if (messages == null || maxTokens == null) {
1946+
List<String> missing = new ArrayList<>();
1947+
if (messages == null) {
1948+
missing.add("messages -> []");
1949+
messages = List.of();
1950+
}
1951+
if (maxTokens == null) {
1952+
missing.add("maxTokens -> 0");
1953+
maxTokens = 0;
1954+
}
1955+
logger.warn("CreateMessageRequest: missing required fields during deserialization: {}",
1956+
String.join(", ", missing));
1957+
}
1958+
return new CreateMessageRequest(messages, modelPreferences, systemPrompt, includeContext, temperature,
1959+
maxTokens, stopSequences, metadata, meta);
1960+
}
1961+
18951962
// backwards compatibility constructor
18961963
public CreateMessageRequest(List<SamplingMessage> messages, ModelPreferences modelPreferences,
18971964
String systemPrompt, ContextInclusionStrategy includeContext, Double temperature, Integer maxTokens,
@@ -2136,6 +2203,10 @@ public CreateMessageResult build() {
21362203
* @param requestedSchema A restricted subset of JSON Schema. Only top-level
21372204
* properties are allowed, without nesting
21382205
* @param meta See specification for notes on _meta usage
2206+
* <p>
2207+
* Note: {@code message} and {@code requestedSchema} are required by the MCP
2208+
* specification. Deserialization accepts missing values and substitutes defaults to
2209+
* avoid breaking existing integrations that may omit these fields.
21392210
*/
21402211
@JsonInclude(JsonInclude.Include.NON_ABSENT)
21412212
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -2149,6 +2220,26 @@ public record ElicitRequest( // @formatter:off
21492220
Assert.notNull(requestedSchema, "requestedSchema must not be null");
21502221
}
21512222

2223+
@JsonCreator
2224+
static ElicitRequest fromJson(@JsonProperty("message") String message,
2225+
@JsonProperty("requestedSchema") Map<String, Object> requestedSchema,
2226+
@JsonProperty("_meta") Map<String, Object> meta) {
2227+
if (message == null || requestedSchema == null) {
2228+
List<String> missing = new ArrayList<>();
2229+
if (message == null) {
2230+
missing.add("message -> ''");
2231+
message = "";
2232+
}
2233+
if (requestedSchema == null) {
2234+
missing.add("requestedSchema -> {}");
2235+
requestedSchema = Map.of();
2236+
}
2237+
logger.warn("ElicitRequest: missing required fields during deserialization: {}",
2238+
String.join(", ", missing));
2239+
}
2240+
return new ElicitRequest(message, requestedSchema, meta);
2241+
}
2242+
21522243
// backwards compatibility constructor
21532244
public ElicitRequest(String message, Map<String, Object> requestedSchema) {
21542245
this(message, requestedSchema, null);
@@ -2340,6 +2431,10 @@ public record PaginatedResult(@JsonProperty("nextCursor") String nextCursor) {
23402431
* @param total An optional total amount of work to be done, if known.
23412432
* @param message An optional message providing additional context about the progress.
23422433
* @param meta See specification for notes on _meta usage
2434+
* <p>
2435+
* Note: {@code progressToken} and {@code progress} are required by the MCP
2436+
* specification. Deserialization accepts missing values and substitutes defaults to
2437+
* avoid breaking existing integrations that may omit these fields.
23432438
*/
23442439
@JsonInclude(JsonInclude.Include.NON_ABSENT)
23452440
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -2355,6 +2450,26 @@ public record ProgressNotification( // @formatter:off
23552450
Assert.notNull(progress, "progress must not be null");
23562451
}
23572452

2453+
@JsonCreator
2454+
static ProgressNotification fromJson(@JsonProperty("progressToken") Object progressToken,
2455+
@JsonProperty("progress") Double progress, @JsonProperty("total") Double total,
2456+
@JsonProperty("message") String message, @JsonProperty("_meta") Map<String, Object> meta) {
2457+
if (progressToken == null || progress == null) {
2458+
List<String> missing = new ArrayList<>();
2459+
if (progressToken == null) {
2460+
missing.add("progressToken -> ''");
2461+
progressToken = "";
2462+
}
2463+
if (progress == null) {
2464+
missing.add("progress -> 0.0");
2465+
progress = 0.0;
2466+
}
2467+
logger.warn("ProgressNotification: missing required fields during deserialization: {}",
2468+
String.join(", ", missing));
2469+
}
2470+
return new ProgressNotification(progressToken, progress, total, message, meta);
2471+
}
2472+
23582473
public ProgressNotification(Object progressToken, double progress, Double total, String message) {
23592474
this(progressToken, progress, total, message, null);
23602475
}
@@ -2432,6 +2547,10 @@ public ResourcesUpdatedNotification(String uri) {
24322547
* @param logger The logger that generated the message.
24332548
* @param data JSON-serializable logging data.
24342549
* @param meta See specification for notes on _meta usage
2550+
* <p>
2551+
* Note: {@code level} and {@code data} are required by the MCP specification.
2552+
* Deserialization accepts missing values and substitutes defaults to avoid breaking
2553+
* existing integrations that may omit these fields.
24352554
*/
24362555
@JsonInclude(JsonInclude.Include.NON_ABSENT)
24372556
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -2446,6 +2565,26 @@ public record LoggingMessageNotification( // @formatter:off
24462565
Assert.notNull(data, "data must not be null");
24472566
}
24482567

2568+
@JsonCreator
2569+
static LoggingMessageNotification fromJson(@JsonProperty("level") LoggingLevel level,
2570+
@JsonProperty("logger") String loggerName, @JsonProperty("data") String data,
2571+
@JsonProperty("_meta") Map<String, Object> meta) {
2572+
if (level == null || data == null) {
2573+
List<String> missing = new ArrayList<>();
2574+
if (level == null) {
2575+
missing.add("level -> INFO");
2576+
level = LoggingLevel.INFO;
2577+
}
2578+
if (data == null) {
2579+
missing.add("data -> ''");
2580+
data = "";
2581+
}
2582+
McpSchema.logger.warn("LoggingMessageNotification: missing required fields during deserialization: {}",
2583+
String.join(", ", missing));
2584+
}
2585+
return new LoggingMessageNotification(level, loggerName, data, meta);
2586+
}
2587+
24492588
// backwards compatibility constructor
24502589
public LoggingMessageNotification(LoggingLevel level, String logger, String data) {
24512590
this(level, logger, data, null);

mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,16 @@ void testCallToolResultBuilderWithErrorResult() throws Exception {
13461346
{"content":[{"type":"text","text":"Error: Operation failed"}],"isError":true}"""));
13471347
}
13481348

1349+
@Test
1350+
void testCallToolResultDeserializationWithMissingContent() throws Exception {
1351+
McpSchema.CallToolResult result = JSON_MAPPER.readValue("""
1352+
{"isError":false}""", McpSchema.CallToolResult.class);
1353+
1354+
assertThat(result).isNotNull();
1355+
assertThat(result.content()).isEmpty();
1356+
assertThat(result.isError()).isFalse();
1357+
}
1358+
13491359
// Sampling Tests
13501360

13511361
@Test
@@ -1382,6 +1392,26 @@ void testCreateMessageRequest() throws Exception {
13821392
{"messages":[{"role":"user","content":{"type":"text","text":"User message"}}],"modelPreferences":{"hints":[{"name":"gpt-4"}],"costPriority":0.3,"speedPriority":0.7,"intelligencePriority":0.9},"systemPrompt":"You are a helpful assistant","includeContext":"thisServer","temperature":0.7,"maxTokens":1000,"stopSequences":["STOP","END"],"metadata":{"session":"test-session"}}"""));
13831393
}
13841394

1395+
@Test
1396+
void testSamplingMessageDeserializationWithMissingFields() throws Exception {
1397+
McpSchema.SamplingMessage message = JSON_MAPPER.readValue("{}", McpSchema.SamplingMessage.class);
1398+
1399+
assertThat(message).isNotNull();
1400+
assertThat(message.role()).isEqualTo(McpSchema.Role.USER);
1401+
assertThat(message.content()).isInstanceOf(McpSchema.TextContent.class);
1402+
}
1403+
1404+
@Test
1405+
void testCreateMessageRequestDeserializationWithMissingRequiredFields() throws Exception {
1406+
McpSchema.CreateMessageRequest request = JSON_MAPPER.readValue("""
1407+
{"systemPrompt":"hello"}""", McpSchema.CreateMessageRequest.class);
1408+
1409+
assertThat(request).isNotNull();
1410+
assertThat(request.messages()).isEmpty();
1411+
assertThat(request.maxTokens()).isZero();
1412+
assertThat(request.systemPrompt()).isEqualTo("hello");
1413+
}
1414+
13851415
@Test
13861416
void testCreateMessageResult() throws Exception {
13871417
McpSchema.TextContent content = new McpSchema.TextContent("Assistant response");
@@ -1455,6 +1485,15 @@ void testCreateElicitationResult() throws Exception {
14551485
{"action":"accept","content":{"foo":"bar"}}"""));
14561486
}
14571487

1488+
@Test
1489+
void testElicitRequestDeserializationWithMissingRequiredFields() throws Exception {
1490+
McpSchema.ElicitRequest request = JSON_MAPPER.readValue("{}", McpSchema.ElicitRequest.class);
1491+
1492+
assertThat(request).isNotNull();
1493+
assertThat(request.message()).isEmpty();
1494+
assertThat(request.requestedSchema()).isEmpty();
1495+
}
1496+
14581497
@Test
14591498
void testElicitRequestWithMeta() throws Exception {
14601499
Map<String, Object> requestedSchema = Map.of("type", "object", "required", List.of("name"), "properties",
@@ -1752,6 +1791,17 @@ void testProgressNotificationDeserialization() throws Exception {
17521791
assertThat(notification.meta()).containsEntry("key", "value");
17531792
}
17541793

1794+
@Test
1795+
void testProgressNotificationDeserializationWithMissingRequiredFields() throws Exception {
1796+
McpSchema.ProgressNotification notification = JSON_MAPPER.readValue("""
1797+
{"total":1.0}""", McpSchema.ProgressNotification.class);
1798+
1799+
assertThat(notification).isNotNull();
1800+
assertThat(notification.progressToken()).isEqualTo("");
1801+
assertThat(notification.progress()).isZero();
1802+
assertThat(notification.total()).isEqualTo(1.0);
1803+
}
1804+
17551805
@Test
17561806
void testProgressNotificationWithoutMessage() throws Exception {
17571807
McpSchema.ProgressNotification notification = new McpSchema.ProgressNotification("progress-token-789", 0.25,
@@ -1765,4 +1815,15 @@ void testProgressNotificationWithoutMessage() throws Exception {
17651815
{"progressToken":"progress-token-789","progress":0.25}"""));
17661816
}
17671817

1818+
@Test
1819+
void testLoggingMessageNotificationDeserializationWithMissingRequiredFields() throws Exception {
1820+
McpSchema.LoggingMessageNotification notification = JSON_MAPPER.readValue("""
1821+
{"logger":"my-logger"}""", McpSchema.LoggingMessageNotification.class);
1822+
1823+
assertThat(notification).isNotNull();
1824+
assertThat(notification.level()).isEqualTo(McpSchema.LoggingLevel.INFO);
1825+
assertThat(notification.logger()).isEqualTo("my-logger");
1826+
assertThat(notification.data()).isEmpty();
1827+
}
1828+
17681829
}

0 commit comments

Comments
 (0)