From 3de287223dae65fade88a028769473f7ac60fe97 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Wed, 16 Jul 2025 11:06:20 +0200 Subject: [PATCH] feat: enforce MCP request ID validation requirements Signed-off-by: Christian Tzolov --- .../modelcontextprotocol/spec/McpSchema.java | 11 +++ .../io/modelcontextprotocol/util/Assert.java | 13 +++ .../spec/JSONRPCRequestMcpValidationTest.java | 90 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 mcp/src/test/java/io/modelcontextprotocol/spec/JSONRPCRequestMcpValidationTest.java diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 4a570aea0..32eba4f01 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -218,6 +218,17 @@ public record JSONRPCRequest( // @formatter:off @JsonProperty("method") String method, @JsonProperty("id") Object id, @JsonProperty("params") Object params) implements JSONRPCMessage { // @formatter:on + + /** + * Constructor that validates MCP-specific ID requirements. Unlike base JSON-RPC, + * MCP requires that: (1) Requests MUST include a string or integer ID; (2) The ID + * MUST NOT be null + */ + public JSONRPCRequest { + Assert.notNull(id, "MCP requests MUST include an ID - null IDs are not allowed"); + Assert.isTrue(id instanceof String || id instanceof Integer || id instanceof Long, + "MCP requests MUST have an ID that is either a string or integer"); + } } /** diff --git a/mcp/src/main/java/io/modelcontextprotocol/util/Assert.java b/mcp/src/main/java/io/modelcontextprotocol/util/Assert.java index d68188c6f..1fa6b3058 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/util/Assert.java +++ b/mcp/src/main/java/io/modelcontextprotocol/util/Assert.java @@ -76,4 +76,17 @@ public static boolean hasText(@Nullable String str) { return (str != null && !str.isBlank()); } + /** + * Assert a boolean expression, throwing an {@code IllegalArgumentException} if the + * expression evaluates to {@code false}. + * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if {@code expression} is {@code false} + */ + public static void isTrue(boolean expression, String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + } diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/JSONRPCRequestMcpValidationTest.java b/mcp/src/test/java/io/modelcontextprotocol/spec/JSONRPCRequestMcpValidationTest.java new file mode 100644 index 000000000..19e1c9ae6 --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/JSONRPCRequestMcpValidationTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package io.modelcontextprotocol.spec; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for MCP-specific validation of JSONRPCRequest ID requirements. + * + * @author Christian Tzolov + */ +public class JSONRPCRequestMcpValidationTest { + + @Test + public void testValidStringId() { + assertDoesNotThrow(() -> { + var request = new McpSchema.JSONRPCRequest("2.0", "test/method", "string-id", null); + assertEquals("string-id", request.id()); + }); + } + + @Test + public void testValidIntegerId() { + assertDoesNotThrow(() -> { + var request = new McpSchema.JSONRPCRequest("2.0", "test/method", 123, null); + assertEquals(123, request.id()); + }); + } + + @Test + public void testValidLongId() { + assertDoesNotThrow(() -> { + var request = new McpSchema.JSONRPCRequest("2.0", "test/method", 123L, null); + assertEquals(123L, request.id()); + }); + } + + @Test + public void testNullIdThrowsException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new McpSchema.JSONRPCRequest("2.0", "test/method", null, null); + }); + + assertTrue(exception.getMessage().contains("MCP requests MUST include an ID")); + assertTrue(exception.getMessage().contains("null IDs are not allowed")); + } + + @Test + public void testDoubleIdTypeThrowsException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new McpSchema.JSONRPCRequest("2.0", "test/method", 123.45, null); + }); + + assertTrue(exception.getMessage().contains("MCP requests MUST have an ID that is either a string or integer")); + assertTrue(exception.getMessage().contains("Double")); + } + + @Test + public void testBooleanIdThrowsException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new McpSchema.JSONRPCRequest("2.0", "test/method", true, null); + }); + + assertTrue(exception.getMessage().contains("MCP requests MUST have an ID that is either a string or integer")); + assertTrue(exception.getMessage().contains("Boolean")); + } + + @Test + public void testArrayIdThrowsException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new McpSchema.JSONRPCRequest("2.0", "test/method", new String[] { "array" }, null); + }); + + assertTrue(exception.getMessage().contains("MCP requests MUST have an ID that is either a string or integer")); + } + + @Test + public void testObjectIdThrowsException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new McpSchema.JSONRPCRequest("2.0", "test/method", new Object(), null); + }); + + assertTrue(exception.getMessage().contains("MCP requests MUST have an ID that is either a string or integer")); + assertTrue(exception.getMessage().contains("Object")); + } + +}