diff --git a/MIGRATION-2.0.md b/MIGRATION-2.0.md index 273421189..d8c7bf28c 100644 --- a/MIGRATION-2.0.md +++ b/MIGRATION-2.0.md @@ -77,6 +77,43 @@ The `Tool` record now models `inputSchema` (and `outputSchema`) as arbitrary JSO - Java code that used `Tool.inputSchema()` as a `JsonSchema` must switch to `Map` (or copy into your own schema wrapper). - `Tool.Builder.inputSchema(JsonSchema)` remains as a **deprecated** helper that maps the old record into a map; prefer `inputSchema(Map)` or `inputSchema(McpJsonMapper, String)`. +### Required MCP spec fields are enforced at construction time; builders require them upfront + +The following records assert that their required fields are non-null at construction time. Passing `null` throws `IllegalArgumentException` immediately, rather than producing a structurally invalid object that fails later during serialization or protocol handling. + +| Record | Required (non-null) fields | +|--------|---------------------------| +| `JSONRPCResponse.JSONRPCError` | `code`, `message` | +| `CallToolResult` | `content` | +| `SamplingMessage` | `role`, `content` | +| `CreateMessageRequest` | `messages`, `maxTokens` | +| `ElicitRequest` | `message`, `requestedSchema` | +| `ProgressNotification` | `progressToken`, `progress` | +| `LoggingMessageNotification` | `level`, `data` | + +**Action:** Audit any code that constructs these records with potentially-null values and provide valid, non-null arguments. + +**Wire deserialization is lenient** + +Deserialization substitutes safe defaults for absent required fields instead of failing. A `WARN` is logged for every field that was defaulted. `JSONRPCResponse.JSONRPCError` is excluded — malformed JSON-RPC error envelopes still fail immediately. + +#### Builder API changes + +The builder factory methods for several records now require the mandatory fields as arguments, making it impossible to obtain a builder that is already missing required state. The old no-arg `builder()` factory and the public no-arg `Builder()` constructor are deprecated and will be removed in a future release. + +| Type | Old (deprecated) | New | +|------|-----------------|-----| +| `CreateMessageRequest` | `CreateMessageRequest.builder().messages(m).maxTokens(n)` | `CreateMessageRequest.builder(m, n)` | +| `ElicitRequest` | `ElicitRequest.builder().message(m).requestedSchema(s)` | `ElicitRequest.builder(m, s)` | +| `LoggingMessageNotification` | `LoggingMessageNotification.builder().level(l).data(d)` | `LoggingMessageNotification.builder(l, d)` | + +Two records that previously had no builder now have one with the same required-first convention: + +- `ProgressNotification.builder(progressToken, progress)` — optional: `.total(Double)`, `.message(String)`, `.meta(Map)` +- `JSONRPCResponse.JSONRPCError.builder(code, message)` — optional: `.data(Object)` + +**Note:** A *missing* `level` field on the wire is handled — it defaults to `INFO` (see the wire-defaults table above). However, an *unrecognized* level string still deserializes to `null` (see the `LoggingLevel` section above), which will then fail the canonical constructor. Ensure clients and servers send only recognized level strings. + ### Optional JSON Schema validation on `tools/call` (server) When a `JsonSchemaValidator` is available (including the default from `McpJsonDefaults.getSchemaValidator()` when you do not configure one explicitly) and `validateToolInputs` is left at its default of `true`, the server validates incoming tool arguments against `tool.inputSchema()` before invoking the tool. Failed validation produces a `CallToolResult` with `isError` set and a textual error in the content. diff --git a/conformance-tests/server-servlet/src/main/java/io/modelcontextprotocol/conformance/server/ConformanceServlet.java b/conformance-tests/server-servlet/src/main/java/io/modelcontextprotocol/conformance/server/ConformanceServlet.java index 25ec2c106..71704e2a7 100644 --- a/conformance-tests/server-servlet/src/main/java/io/modelcontextprotocol/conformance/server/ConformanceServlet.java +++ b/conformance-tests/server-servlet/src/main/java/io/modelcontextprotocol/conformance/server/ConformanceServlet.java @@ -237,18 +237,14 @@ private static List createToolSpecs() { .callHandler((exchange, request) -> { logger.info("Tool 'test_tool_with_logging' called"); // Send log notifications - exchange.loggingNotification(LoggingMessageNotification.builder() - .level(LoggingLevel.INFO) - .data("Tool execution started") - .build()); - exchange.loggingNotification(LoggingMessageNotification.builder() - .level(LoggingLevel.INFO) - .data("Tool processing data") - .build()); - exchange.loggingNotification(LoggingMessageNotification.builder() - .level(LoggingLevel.INFO) - .data("Tool execution completed") - .build()); + exchange.loggingNotification( + LoggingMessageNotification.builder(LoggingLevel.INFO, "Tool execution started") + .build()); + exchange.loggingNotification( + LoggingMessageNotification.builder(LoggingLevel.INFO, "Tool processing data").build()); + exchange.loggingNotification( + LoggingMessageNotification.builder(LoggingLevel.INFO, "Tool execution completed") + .build()); return CallToolResult.builder() .content(List.of(new TextContent("Tool execution completed with logging"))) .isError(false) @@ -335,9 +331,8 @@ private static List createToolSpecs() { String prompt = (String) request.arguments().get("prompt"); // Request sampling from client - CreateMessageRequest samplingRequest = CreateMessageRequest.builder() - .messages(List.of(new SamplingMessage(Role.USER, new TextContent(prompt)))) - .maxTokens(100) + CreateMessageRequest samplingRequest = CreateMessageRequest + .builder(List.of(new SamplingMessage(Role.USER, new TextContent(prompt))), 100) .build(); CreateMessageResult response = exchange.createMessage(samplingRequest); diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index e5f57bad8..f7cb4d619 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -999,12 +999,9 @@ private McpRequestHandler completionCompleteRequestHan .message("Prompt not found: " + promptReference.name()) .build()); } - if (!promptSpec.prompt() - .arguments() - .stream() - .filter(arg -> arg.name().equals(argumentName)) - .findFirst() - .isPresent()) { + List arguments = promptSpec.prompt().arguments(); + if (arguments == null + || !arguments.stream().filter(arg -> arg.name().equals(argumentName)).findFirst().isPresent()) { logger.warn("Argument not found: {} in prompt: {}", argumentName, promptReference.name()); diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java index 18fc85786..46fdf0aab 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java @@ -744,12 +744,9 @@ private McpStatelessRequestHandler completionCompleteR .message("Prompt not found: " + promptReference.name()) .build()); } - if (!promptSpec.prompt() - .arguments() - .stream() - .filter(arg -> arg.name().equals(argumentName)) - .findFirst() - .isPresent()) { + List arguments = promptSpec.prompt().arguments(); + if (arguments == null + || !arguments.stream().filter(arg -> arg.name().equals(argumentName)).findFirst().isPresent()) { logger.warn("Argument not found: {} in prompt: {}", argumentName, promptReference.name()); diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 72376929c..eca5707f8 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -296,6 +296,41 @@ public record JSONRPCError( // @formatter:off @JsonProperty("code") Integer code, @JsonProperty("message") String message, @JsonProperty("data") Object data) { // @formatter:on + + public JSONRPCError { + Assert.notNull(code, "code must not be null"); + Assert.notNull(message, "message must not be null"); + } + + public static Builder builder(int code, String message) { + return new Builder(code, message); + } + + public static class Builder { + + private final Integer code; + + private final String message; + + private Object data; + + private Builder(int code, String message) { + Assert.notNull(message, "message must not be null"); + this.code = code; + this.message = message; + } + + public Builder data(Object data) { + this.data = data; + return this; + } + + public JSONRPCError build() { + return new JSONRPCError(code, message, data); + } + + } + } } @@ -320,6 +355,37 @@ public record InitializeRequest( // @formatter:off @JsonProperty("clientInfo") Implementation clientInfo, @JsonProperty("_meta") Map meta) implements Request { // @formatter:on + public InitializeRequest { + Assert.notNull(protocolVersion, "protocolVersion must not be null"); + Assert.notNull(capabilities, "capabilities must not be null"); + Assert.notNull(clientInfo, "clientInfo must not be null"); + } + + @JsonCreator + static InitializeRequest fromJson(@JsonProperty("protocolVersion") String protocolVersion, + @JsonProperty("capabilities") ClientCapabilities capabilities, + @JsonProperty("clientInfo") Implementation clientInfo, + @JsonProperty("_meta") Map meta) { + if (protocolVersion == null || capabilities == null || clientInfo == null) { + List missing = new ArrayList<>(); + if (protocolVersion == null) { + missing.add("protocolVersion -> ''"); + protocolVersion = ""; + } + if (capabilities == null) { + missing.add("capabilities -> {}"); + capabilities = new ClientCapabilities(null, null, null, null); + } + if (clientInfo == null) { + missing.add("clientInfo -> {name='', version=''}"); + clientInfo = new Implementation("", ""); + } + logger.warn("InitializeRequest: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new InitializeRequest(protocolVersion, capabilities, clientInfo, meta); + } + public InitializeRequest(String protocolVersion, ClientCapabilities capabilities, Implementation clientInfo) { this(protocolVersion, capabilities, clientInfo, null); } @@ -349,6 +415,37 @@ public record InitializeResult( // @formatter:off @JsonProperty("instructions") String instructions, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + public InitializeResult { + Assert.notNull(protocolVersion, "protocolVersion must not be null"); + Assert.notNull(capabilities, "capabilities must not be null"); + Assert.notNull(serverInfo, "serverInfo must not be null"); + } + + @JsonCreator + static InitializeResult fromJson(@JsonProperty("protocolVersion") String protocolVersion, + @JsonProperty("capabilities") ServerCapabilities capabilities, + @JsonProperty("serverInfo") Implementation serverInfo, + @JsonProperty("instructions") String instructions, @JsonProperty("_meta") Map meta) { + if (protocolVersion == null || capabilities == null || serverInfo == null) { + List missing = new ArrayList<>(); + if (protocolVersion == null) { + missing.add("protocolVersion -> ''"); + protocolVersion = ""; + } + if (capabilities == null) { + missing.add("capabilities -> {}"); + capabilities = new ServerCapabilities(null, null, null, null, null, null); + } + if (serverInfo == null) { + missing.add("serverInfo -> {name='', version=''}"); + serverInfo = new Implementation("", ""); + } + logger.warn("InitializeResult: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new InitializeResult(protocolVersion, capabilities, serverInfo, instructions, meta); + } + public InitializeResult(String protocolVersion, ServerCapabilities capabilities, Implementation serverInfo, String instructions) { this(protocolVersion, capabilities, serverInfo, instructions, null); @@ -667,7 +764,31 @@ public ServerCapabilities build() { public record Implementation( // @formatter:off @JsonProperty("name") String name, @JsonProperty("title") String title, - @JsonProperty("version") String version) implements Identifier { // @formatter:on + @JsonProperty("version") String version) implements Identifier { // @formatter:on + + public Implementation { + Assert.notNull(name, "name must not be null"); + Assert.notNull(version, "version must not be null"); + } + + @JsonCreator + static Implementation fromJson(@JsonProperty("name") String name, @JsonProperty("title") String title, + @JsonProperty("version") String version) { + if (name == null || version == null) { + List missing = new ArrayList<>(); + if (name == null) { + missing.add("name -> ''"); + name = ""; + } + if (version == null) { + missing.add("version -> ''"); + version = ""; + } + logger.warn("Implementation: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new Implementation(name, title, version); + } public Implementation(String name, String version) { this(name, null, version); @@ -985,6 +1106,21 @@ public record ListResourcesResult( // @formatter:off @JsonProperty("nextCursor") String nextCursor, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + public ListResourcesResult { + Assert.notNull(resources, "resources must not be null"); + } + + @JsonCreator + static ListResourcesResult fromJson(@JsonProperty("resources") List resources, + @JsonProperty("nextCursor") String nextCursor, @JsonProperty("_meta") Map meta) { + if (resources == null) { + logger.warn( + "ListResourcesResult: missing required field 'resources' during deserialization, using default []"); + resources = List.of(); + } + return new ListResourcesResult(resources, nextCursor, meta); + } + public ListResourcesResult(List resources, String nextCursor) { this(resources, nextCursor, null); } @@ -1005,6 +1141,22 @@ public record ListResourceTemplatesResult( // @formatter:off @JsonProperty("nextCursor") String nextCursor, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + public ListResourceTemplatesResult { + Assert.notNull(resourceTemplates, "resourceTemplates must not be null"); + } + + @JsonCreator + static ListResourceTemplatesResult fromJson( + @JsonProperty("resourceTemplates") List resourceTemplates, + @JsonProperty("nextCursor") String nextCursor, @JsonProperty("_meta") Map meta) { + if (resourceTemplates == null) { + logger.warn( + "ListResourceTemplatesResult: missing required field 'resourceTemplates' during deserialization, using default []"); + resourceTemplates = List.of(); + } + return new ListResourceTemplatesResult(resourceTemplates, nextCursor, meta); + } + public ListResourceTemplatesResult(List resourceTemplates, String nextCursor) { this(resourceTemplates, nextCursor, null); } @@ -1023,6 +1175,21 @@ public record ReadResourceRequest( // @formatter:off @JsonProperty("uri") String uri, @JsonProperty("_meta") Map meta) implements Request { // @formatter:on + public ReadResourceRequest { + Assert.notNull(uri, "uri must not be null"); + } + + @JsonCreator + static ReadResourceRequest fromJson(@JsonProperty("uri") String uri, + @JsonProperty("_meta") Map meta) { + if (uri == null) { + logger + .warn("ReadResourceRequest: missing required field 'uri' during deserialization, using default ''"); + uri = ""; + } + return new ReadResourceRequest(uri, meta); + } + public ReadResourceRequest(String uri) { this(uri, null); } @@ -1040,6 +1207,21 @@ public record ReadResourceResult( // @formatter:off @JsonProperty("contents") List contents, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + public ReadResourceResult { + Assert.notNull(contents, "contents must not be null"); + } + + @JsonCreator + static ReadResourceResult fromJson(@JsonProperty("contents") List contents, + @JsonProperty("_meta") Map meta) { + if (contents == null) { + logger.warn( + "ReadResourceResult: missing required field 'contents' during deserialization, using default []"); + contents = List.of(); + } + return new ReadResourceResult(contents, meta); + } + public ReadResourceResult(List contents) { this(contents, null); } @@ -1059,6 +1241,20 @@ public record SubscribeRequest( // @formatter:off @JsonProperty("uri") String uri, @JsonProperty("_meta") Map meta) implements Request { // @formatter:on + public SubscribeRequest { + Assert.notNull(uri, "uri must not be null"); + } + + @JsonCreator + static SubscribeRequest fromJson(@JsonProperty("uri") String uri, + @JsonProperty("_meta") Map meta) { + if (uri == null) { + logger.warn("SubscribeRequest: missing required field 'uri' during deserialization, using default ''"); + uri = ""; + } + return new SubscribeRequest(uri, meta); + } + public SubscribeRequest(String uri) { this(uri, null); } @@ -1077,6 +1273,21 @@ public record UnsubscribeRequest( // @formatter:off @JsonProperty("uri") String uri, @JsonProperty("_meta") Map meta) implements Request { // @formatter:on + public UnsubscribeRequest { + Assert.notNull(uri, "uri must not be null"); + } + + @JsonCreator + static UnsubscribeRequest fromJson(@JsonProperty("uri") String uri, + @JsonProperty("_meta") Map meta) { + if (uri == null) { + logger + .warn("UnsubscribeRequest: missing required field 'uri' during deserialization, using default ''"); + uri = ""; + } + return new UnsubscribeRequest(uri, meta); + } + public UnsubscribeRequest(String uri) { this(uri, null); } @@ -1121,6 +1332,30 @@ public record TextResourceContents( // @formatter:off @JsonProperty("text") String text, @JsonProperty("_meta") Map meta) implements ResourceContents { // @formatter:on + public TextResourceContents { + Assert.notNull(uri, "uri must not be null"); + Assert.notNull(text, "text must not be null"); + } + + @JsonCreator + static TextResourceContents fromJson(@JsonProperty("uri") String uri, @JsonProperty("mimeType") String mimeType, + @JsonProperty("text") String text, @JsonProperty("_meta") Map meta) { + if (uri == null || text == null) { + List missing = new ArrayList<>(); + if (uri == null) { + missing.add("uri -> ''"); + uri = ""; + } + if (text == null) { + missing.add("text -> ''"); + text = ""; + } + logger.warn("TextResourceContents: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new TextResourceContents(uri, mimeType, text, meta); + } + public TextResourceContents(String uri, String mimeType, String text) { this(uri, mimeType, text, null); } @@ -1144,6 +1379,30 @@ public record BlobResourceContents( // @formatter:off @JsonProperty("blob") String blob, @JsonProperty("_meta") Map meta) implements ResourceContents { // @formatter:on + public BlobResourceContents { + Assert.notNull(uri, "uri must not be null"); + Assert.notNull(blob, "blob must not be null"); + } + + @JsonCreator + static BlobResourceContents fromJson(@JsonProperty("uri") String uri, @JsonProperty("mimeType") String mimeType, + @JsonProperty("blob") String blob, @JsonProperty("_meta") Map meta) { + if (uri == null || blob == null) { + List missing = new ArrayList<>(); + if (uri == null) { + missing.add("uri -> ''"); + uri = ""; + } + if (blob == null) { + missing.add("blob -> ''"); + blob = ""; + } + logger.warn("BlobResourceContents: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new BlobResourceContents(uri, mimeType, blob, meta); + } + public BlobResourceContents(String uri, String mimeType, String blob) { this(uri, mimeType, blob, null); } @@ -1170,6 +1429,22 @@ public record Prompt( // @formatter:off @JsonProperty("arguments") List arguments, @JsonProperty("_meta") Map meta) implements Identifier { // @formatter:on + public Prompt { + Assert.notNull(name, "name must not be null"); + } + + @JsonCreator + static Prompt fromJson(@JsonProperty("name") String name, @JsonProperty("title") String title, + @JsonProperty("description") String description, + @JsonProperty("arguments") List arguments, + @JsonProperty("_meta") Map meta) { + if (name == null) { + logger.warn("Prompt: missing required field 'name' during deserialization, using default ''"); + name = ""; + } + return new Prompt(name, title, description, arguments, meta); + } + public Prompt(String name, String description, List arguments) { this(name, null, description, arguments, null); } @@ -1214,6 +1489,29 @@ public PromptArgument(String name, String description, Boolean required) { public record PromptMessage( // @formatter:off @JsonProperty("role") Role role, @JsonProperty("content") Content content) { // @formatter:on + + public PromptMessage { + Assert.notNull(role, "role must not be null"); + Assert.notNull(content, "content must not be null"); + } + + @JsonCreator + static PromptMessage fromJson(@JsonProperty("role") Role role, @JsonProperty("content") Content content) { + if (role == null || content == null) { + List missing = new ArrayList<>(); + if (role == null) { + missing.add("role -> 'user'"); + role = Role.USER; + } + if (content == null) { + missing.add("content -> ''"); + content = new TextContent(""); + } + logger.warn("PromptMessage: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new PromptMessage(role, content); + } } /** @@ -1231,6 +1529,21 @@ public record ListPromptsResult( // @formatter:off @JsonProperty("nextCursor") String nextCursor, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + public ListPromptsResult { + Assert.notNull(prompts, "prompts must not be null"); + } + + @JsonCreator + static ListPromptsResult fromJson(@JsonProperty("prompts") List prompts, + @JsonProperty("nextCursor") String nextCursor, @JsonProperty("_meta") Map meta) { + if (prompts == null) { + logger.warn( + "ListPromptsResult: missing required field 'prompts' during deserialization, using default []"); + prompts = List.of(); + } + return new ListPromptsResult(prompts, nextCursor, meta); + } + public ListPromptsResult(List prompts, String nextCursor) { this(prompts, nextCursor, null); } @@ -1250,6 +1563,21 @@ public record GetPromptRequest( // @formatter:off @JsonProperty("arguments") Map arguments, @JsonProperty("_meta") Map meta) implements Request { // @formatter:on + public GetPromptRequest { + Assert.notNull(name, "name must not be null"); + } + + @JsonCreator + static GetPromptRequest fromJson(@JsonProperty("name") String name, + @JsonProperty("arguments") Map arguments, + @JsonProperty("_meta") Map meta) { + if (name == null) { + logger.warn("GetPromptRequest: missing required field 'name' during deserialization, using default ''"); + name = ""; + } + return new GetPromptRequest(name, arguments, meta); + } + public GetPromptRequest(String name, Map arguments) { this(name, arguments, null); } @@ -1269,6 +1597,22 @@ public record GetPromptResult( // @formatter:off @JsonProperty("messages") List messages, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + public GetPromptResult { + Assert.notNull(messages, "messages must not be null"); + } + + @JsonCreator + static GetPromptResult fromJson(@JsonProperty("description") String description, + @JsonProperty("messages") List messages, + @JsonProperty("_meta") Map meta) { + if (messages == null) { + logger.warn( + "GetPromptResult: missing required field 'messages' during deserialization, using default []"); + messages = List.of(); + } + return new GetPromptResult(description, messages, meta); + } + public GetPromptResult(String description, List messages) { this(description, messages, null); } @@ -1292,6 +1636,20 @@ public record ListToolsResult( // @formatter:off @JsonProperty("nextCursor") String nextCursor, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + public ListToolsResult { + Assert.notNull(tools, "tools must not be null"); + } + + @JsonCreator + static ListToolsResult fromJson(@JsonProperty("tools") List tools, + @JsonProperty("nextCursor") String nextCursor, @JsonProperty("_meta") Map meta) { + if (tools == null) { + logger.warn("ListToolsResult: missing required field 'tools' during deserialization, using default []"); + tools = List.of(); + } + return new ListToolsResult(tools, nextCursor, meta); + } + public ListToolsResult(List tools, String nextCursor) { this(tools, nextCursor, null); } @@ -1369,10 +1727,45 @@ public record Tool( // @formatter:off @JsonProperty("annotations") ToolAnnotations annotations, @JsonProperty("_meta") Map meta) { // @formatter:on + public Tool { + Assert.notNull(name, "name must not be null"); + Assert.notNull(inputSchema, "inputSchema must not be null"); + } + + @JsonCreator + static Tool fromJson(@JsonProperty("name") String name, @JsonProperty("title") String title, + @JsonProperty("description") String description, + @JsonProperty("inputSchema") Map inputSchema, + @JsonProperty("outputSchema") Map outputSchema, + @JsonProperty("annotations") ToolAnnotations annotations, + @JsonProperty("_meta") Map meta) { + if (name == null || inputSchema == null) { + List missing = new ArrayList<>(); + if (name == null) { + missing.add("name -> ''"); + name = ""; + } + if (inputSchema == null) { + missing.add("inputSchema -> {}"); + inputSchema = Map.of(); + } + logger.warn("Tool: missing required fields during deserialization: {}", String.join(", ", missing)); + } + return new Tool(name, title, description, inputSchema, outputSchema, annotations, meta); + } + + /** + * @deprecated Use {@link #builder(String)} instead. + */ + @Deprecated public static Builder builder() { return new Builder(); } + public static Builder builder(String name) { + return new Builder(name); + } + public static class Builder { private String name; @@ -1389,6 +1782,18 @@ public static class Builder { private Map meta; + /** + * @deprecated Use {@link Tool#builder(String)} instead. + */ + @Deprecated + public Builder() { + } + + private Builder(String name) { + Assert.hasText(name, "name must not be empty"); + this.name = name; + } + public Builder name(String name) { this.name = name; return this; @@ -1457,6 +1862,9 @@ public Builder meta(Map meta) { public Tool build() { Assert.hasText(name, "name must not be empty"); + if (inputSchema == null) { + inputSchema = Map.of("type", "object"); + } return new Tool(name, title, description, inputSchema, outputSchema, annotations, meta); } @@ -1489,6 +1897,21 @@ public record CallToolRequest( // @formatter:off @JsonProperty("arguments") Map arguments, @JsonProperty("_meta") Map meta) implements Request { // @formatter:on + public CallToolRequest { + Assert.notNull(name, "name must not be null"); + } + + @JsonCreator + static CallToolRequest fromJson(@JsonProperty("name") String name, + @JsonProperty("arguments") Map arguments, + @JsonProperty("_meta") Map meta) { + if (name == null) { + logger.warn("CallToolRequest: missing required field 'name' during deserialization, using default ''"); + name = ""; + } + return new CallToolRequest(name, arguments, meta); + } + public CallToolRequest(McpJsonMapper jsonMapper, String name, String jsonArguments) { this(name, parseJsonArguments(jsonMapper, jsonArguments), null); } @@ -1506,10 +1929,18 @@ private static Map parseJsonArguments(McpJsonMapper jsonMapper, } } + /** + * @deprecated Use {@link #builder(String)} instead. + */ + @Deprecated public static Builder builder() { return new Builder(); } + public static Builder builder(String name) { + return new Builder(name); + } + public static class Builder { private String name; @@ -1518,6 +1949,18 @@ public static class Builder { private Map meta; + /** + * @deprecated Use {@link CallToolRequest#builder(String)} instead. + */ + @Deprecated + public Builder() { + } + + private Builder(String name) { + Assert.hasText(name, "name must not be empty"); + this.name = name; + } + public Builder name(String name) { this.name = name; return this; @@ -1564,6 +2007,10 @@ public CallToolRequest build() { * @param structuredContent An optional JSON object that represents the structured * result of the tool call. * @param meta See specification for notes on _meta usage + *

+ * Note: {@code content} is required by the MCP specification. Deserialization accepts + * a missing value and substitutes an empty list to avoid breaking existing + * integrations that may omit the field. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -1573,6 +2020,21 @@ public record CallToolResult( // @formatter:off @JsonProperty("structuredContent") Object structuredContent, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + public CallToolResult { + Assert.notNull(content, "content must not be null"); + } + + @JsonCreator + static CallToolResult fromJson(@JsonProperty("content") List content, + @JsonProperty("isError") Boolean isError, @JsonProperty("structuredContent") Object structuredContent, + @JsonProperty("_meta") Map meta) { + if (content == null) { + logger.warn("CallToolResult: missing required fields during deserialization: content -> []"); + content = List.of(); + } + return new CallToolResult(content, isError, structuredContent, meta); + } + /** * Creates a builder for {@link CallToolResult}. * @return a new builder instance @@ -1590,6 +2052,14 @@ public static class Builder { private Boolean isError = false; + /** + * @deprecated Use {@link CallToolResult#builder()} factory method instead of + * instantiating the builder directly. + */ + @Deprecated + public Builder() { + } + private Object structuredContent; private Map meta; @@ -1683,6 +2153,7 @@ public Builder meta(Map meta) { * @return a new CallToolResult instance */ public CallToolResult build() { + Assert.notNull(content, "content must not be null"); return new CallToolResult(content, isError, structuredContent, meta); } @@ -1791,12 +2262,39 @@ public static ModelHint of(String name) { * * @param role The sender or recipient of messages and data in a conversation * @param content The content of the message + *

+ * Note: {@code role} and {@code content} are required by the MCP specification. + * Deserialization accepts missing values and substitutes defaults to avoid breaking + * existing integrations that may omit these fields. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record SamplingMessage( // @formatter:off @JsonProperty("role") Role role, @JsonProperty("content") Content content) { // @formatter:on + + public SamplingMessage { + Assert.notNull(role, "role must not be null"); + Assert.notNull(content, "content must not be null"); + } + + @JsonCreator + static SamplingMessage fromJson(@JsonProperty("role") Role role, @JsonProperty("content") Content content) { + if (role == null || content == null) { + List missing = new ArrayList<>(); + if (role == null) { + missing.add("role -> 'user'"); + role = Role.USER; + } + if (content == null) { + missing.add("content -> ''"); + content = new TextContent(""); + } + logger.warn("SamplingMessage: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new SamplingMessage(role, content); + } } /** @@ -1820,6 +2318,10 @@ public record SamplingMessage( // @formatter:off * @param metadata Optional metadata to pass through to the LLM provider. The format * of this metadata is provider-specific * @param meta See specification for notes on _meta usage + *

+ * Note: {@code messages} and {@code maxTokens} are required by the MCP specification. + * Deserialization accepts missing values and substitutes defaults to avoid breaking + * existing integrations that may omit these fields. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -1834,6 +2336,37 @@ public record CreateMessageRequest( // @formatter:off @JsonProperty("metadata") Map metadata, @JsonProperty("_meta") Map meta) implements Request { // @formatter:on + public CreateMessageRequest { + Assert.notNull(messages, "messages must not be null"); + Assert.notNull(maxTokens, "maxTokens must not be null"); + } + + @JsonCreator + static CreateMessageRequest fromJson(@JsonProperty("messages") List messages, + @JsonProperty("modelPreferences") ModelPreferences modelPreferences, + @JsonProperty("systemPrompt") String systemPrompt, + @JsonProperty("includeContext") ContextInclusionStrategy includeContext, + @JsonProperty("temperature") Double temperature, @JsonProperty("maxTokens") Integer maxTokens, + @JsonProperty("stopSequences") List stopSequences, + @JsonProperty("metadata") Map metadata, + @JsonProperty("_meta") Map meta) { + if (messages == null || maxTokens == null) { + List missing = new ArrayList<>(); + if (messages == null) { + missing.add("messages -> []"); + messages = List.of(); + } + if (maxTokens == null) { + missing.add("maxTokens -> 0"); + maxTokens = 0; + } + logger.warn("CreateMessageRequest: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new CreateMessageRequest(messages, modelPreferences, systemPrompt, includeContext, temperature, + maxTokens, stopSequences, metadata, meta); + } + // backwards compatibility constructor public CreateMessageRequest(List messages, ModelPreferences modelPreferences, String systemPrompt, ContextInclusionStrategy includeContext, Double temperature, Integer maxTokens, @@ -1844,17 +2377,24 @@ public CreateMessageRequest(List messages, ModelPreferences mod public enum ContextInclusionStrategy { - @JsonProperty("none") - NONE, @JsonProperty("thisServer") - THIS_SERVER, @JsonProperty("allServers") - ALL_SERVERS - - } + // @formatter:off + @JsonProperty("none") NONE, + @JsonProperty("thisServer") THIS_SERVER, + @JsonProperty("allServers") ALL_SERVERS + } // @formatter:on + /** + * @deprecated Use {@link #builder(List, int)} instead. + */ + @Deprecated public static Builder builder() { return new Builder(); } + public static Builder builder(List messages, int maxTokens) { + return new Builder(messages, maxTokens); + } + public static class Builder { private List messages; @@ -1875,7 +2415,22 @@ public static class Builder { private Map meta; + /** + * @deprecated Use {@link CreateMessageRequest#builder(List, int)} factory + * method instead. + */ + @Deprecated + public Builder() { + } + + private Builder(List messages, int maxTokens) { + Assert.notNull(messages, "messages must not be null"); + this.messages = messages; + this.maxTokens = maxTokens; + } + public Builder messages(List messages) { + Assert.notNull(messages, "messages must not be null"); this.messages = messages; return this; } @@ -1929,6 +2484,8 @@ public Builder progressToken(Object progressToken) { } public CreateMessageRequest build() { + Assert.notNull(messages, "messages must not be null"); + Assert.notNull(maxTokens, "maxTokens must not be null"); return new CreateMessageRequest(messages, modelPreferences, systemPrompt, includeContext, temperature, maxTokens, stopSequences, metadata, meta); } @@ -1957,13 +2514,44 @@ public record CreateMessageResult( // @formatter:off @JsonProperty("stopReason") StopReason stopReason, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + public CreateMessageResult { + Assert.notNull(role, "role must not be null"); + Assert.notNull(content, "content must not be null"); + Assert.notNull(model, "model must not be null"); + } + + @JsonCreator + static CreateMessageResult fromJson(@JsonProperty("role") Role role, @JsonProperty("content") Content content, + @JsonProperty("model") String model, @JsonProperty("stopReason") StopReason stopReason, + @JsonProperty("_meta") Map meta) { + if (role == null || content == null || model == null) { + List missing = new ArrayList<>(); + if (role == null) { + missing.add("role -> 'assistant'"); + role = Role.ASSISTANT; + } + if (content == null) { + missing.add("content -> ''"); + content = new TextContent(""); + } + if (model == null) { + missing.add("model -> ''"); + model = ""; + } + logger.warn("CreateMessageResult: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new CreateMessageResult(role, content, model, stopReason, meta); + } + public enum StopReason { - @JsonProperty("endTurn") - END_TURN("endTurn"), @JsonProperty("stopSequence") - STOP_SEQUENCE("stopSequence"), @JsonProperty("maxTokens") - MAX_TOKENS("maxTokens"), @JsonProperty("unknown") - UNKNOWN("unknown"); + // @formatter:off + @JsonProperty("endTurn") END_TURN("endTurn"), + @JsonProperty("stopSequence") STOP_SEQUENCE("stopSequence"), + @JsonProperty("maxTokens") MAX_TOKENS("maxTokens"), + @JsonProperty("unknown") UNKNOWN("unknown"); + // @formatter:on private final String value; @@ -2040,6 +2628,8 @@ public Builder meta(Map meta) { } public CreateMessageResult build() { + Assert.notNull(content, "content must not be null"); + Assert.notNull(model, "model must not be null"); return new CreateMessageResult(role, content, model, stopReason, meta); } @@ -2055,6 +2645,10 @@ public CreateMessageResult build() { * @param requestedSchema A restricted subset of JSON Schema. Only top-level * properties are allowed, without nesting * @param meta See specification for notes on _meta usage + *

+ * Note: {@code message} and {@code requestedSchema} are required by the MCP + * specification. Deserialization accepts missing values and substitutes defaults to + * avoid breaking existing integrations that may omit these fields. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -2063,15 +2657,48 @@ public record ElicitRequest( // @formatter:off @JsonProperty("requestedSchema") Map requestedSchema, @JsonProperty("_meta") Map meta) implements Request { // @formatter:on + public ElicitRequest { + Assert.notNull(message, "message must not be null"); + Assert.notNull(requestedSchema, "requestedSchema must not be null"); + } + + @JsonCreator + static ElicitRequest fromJson(@JsonProperty("message") String message, + @JsonProperty("requestedSchema") Map requestedSchema, + @JsonProperty("_meta") Map meta) { + if (message == null || requestedSchema == null) { + List missing = new ArrayList<>(); + if (message == null) { + missing.add("message -> ''"); + message = ""; + } + if (requestedSchema == null) { + missing.add("requestedSchema -> {}"); + requestedSchema = Map.of(); + } + logger.warn("ElicitRequest: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new ElicitRequest(message, requestedSchema, meta); + } + // backwards compatibility constructor public ElicitRequest(String message, Map requestedSchema) { this(message, requestedSchema, null); } + /** + * @deprecated Use {@link #builder(String, Map)} instead. + */ + @Deprecated public static Builder builder() { return new Builder(); } + public static Builder builder(String message, Map requestedSchema) { + return new Builder(message, requestedSchema); + } + public static class Builder { private String message; @@ -2080,12 +2707,29 @@ public static class Builder { private Map meta; + /** + * @deprecated Use {@link ElicitRequest#builder(String, Map)} factory method + * instead. + */ + @Deprecated + public Builder() { + } + + private Builder(String message, Map requestedSchema) { + Assert.notNull(message, "message must not be null"); + Assert.notNull(requestedSchema, "requestedSchema must not be null"); + this.message = message; + this.requestedSchema = requestedSchema; + } + public Builder message(String message) { + Assert.notNull(message, "message must not be null"); this.message = message; return this; } public Builder requestedSchema(Map requestedSchema) { + Assert.notNull(requestedSchema, "requestedSchema must not be null"); this.requestedSchema = requestedSchema; return this; } @@ -2104,6 +2748,8 @@ public Builder progressToken(Object progressToken) { } public ElicitRequest build() { + Assert.notNull(message, "message must not be null"); + Assert.notNull(requestedSchema, "requestedSchema must not be null"); return new ElicitRequest(message, requestedSchema, meta); } @@ -2127,15 +2773,29 @@ public record ElicitResult( // @formatter:off @JsonProperty("content") Map content, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on - public enum Action { - - @JsonProperty("accept") - ACCEPT, @JsonProperty("decline") - DECLINE, @JsonProperty("cancel") - CANCEL + public ElicitResult { + Assert.notNull(action, "action must not be null"); + } + @JsonCreator + static ElicitResult fromJson(@JsonProperty("action") Action action, + @JsonProperty("content") Map content, @JsonProperty("_meta") Map meta) { + if (action == null) { + logger.warn( + "ElicitResult: missing required field 'action' during deserialization, using default 'cancel'"); + action = Action.CANCEL; + } + return new ElicitResult(action, content, meta); } + public enum Action { + + // @formatter:off + @JsonProperty("accept") ACCEPT, + @JsonProperty("decline") DECLINE, + @JsonProperty("cancel") CANCEL + } // @formatter:on + // backwards compatibility constructor public ElicitResult(Action action, Map content) { this(action, content, null); @@ -2169,6 +2829,7 @@ public Builder meta(Map meta) { } public ElicitResult build() { + Assert.notNull(action, "action must not be null"); return new ElicitResult(action, content, meta); } @@ -2229,6 +2890,10 @@ public record PaginatedResult(@JsonProperty("nextCursor") String nextCursor) { * @param total An optional total amount of work to be done, if known. * @param message An optional message providing additional context about the progress. * @param meta See specification for notes on _meta usage + *

+ * Note: {@code progressToken} and {@code progress} are required by the MCP + * specification. Deserialization accepts missing values and substitutes defaults to + * avoid breaking existing integrations that may omit these fields. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -2239,9 +2904,78 @@ public record ProgressNotification( // @formatter:off @JsonProperty("message") String message, @JsonProperty("_meta") Map meta) implements Notification { // @formatter:on + public ProgressNotification { + Assert.notNull(progressToken, "progressToken must not be null"); + Assert.notNull(progress, "progress must not be null"); + } + + @JsonCreator + static ProgressNotification fromJson(@JsonProperty("progressToken") Object progressToken, + @JsonProperty("progress") Double progress, @JsonProperty("total") Double total, + @JsonProperty("message") String message, @JsonProperty("_meta") Map meta) { + if (progressToken == null || progress == null) { + List missing = new ArrayList<>(); + if (progressToken == null) { + missing.add("progressToken -> ''"); + progressToken = ""; + } + if (progress == null) { + missing.add("progress -> 0.0"); + progress = 0.0; + } + logger.warn("ProgressNotification: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new ProgressNotification(progressToken, progress, total, message, meta); + } + public ProgressNotification(Object progressToken, double progress, Double total, String message) { this(progressToken, progress, total, message, null); } + + public static Builder builder(Object progressToken, double progress) { + return new Builder(progressToken, progress); + } + + public static class Builder { + + private final Object progressToken; + + private final Double progress; + + private Double total; + + private String message; + + private Map meta; + + private Builder(Object progressToken, double progress) { + Assert.notNull(progressToken, "progressToken must not be null"); + this.progressToken = progressToken; + this.progress = progress; + } + + public Builder total(Double total) { + this.total = total; + return this; + } + + public Builder message(String message) { + this.message = message; + return this; + } + + public Builder meta(Map meta) { + this.meta = meta; + return this; + } + + public ProgressNotification build() { + return new ProgressNotification(progressToken, progress, total, message, meta); + } + + } + } /** @@ -2257,6 +2991,21 @@ public record ResourcesUpdatedNotification(// @formatter:off @JsonProperty("uri") String uri, @JsonProperty("_meta") Map meta) implements Notification { // @formatter:on + public ResourcesUpdatedNotification { + Assert.notNull(uri, "uri must not be null"); + } + + @JsonCreator + static ResourcesUpdatedNotification fromJson(@JsonProperty("uri") String uri, + @JsonProperty("_meta") Map meta) { + if (uri == null) { + logger.warn( + "ResourcesUpdatedNotification: missing required field 'uri' during deserialization, using default ''"); + uri = ""; + } + return new ResourcesUpdatedNotification(uri, meta); + } + public ResourcesUpdatedNotification(String uri) { this(uri, null); } @@ -2272,6 +3021,10 @@ public ResourcesUpdatedNotification(String uri) { * @param logger The logger that generated the message. * @param data JSON-serializable logging data. * @param meta See specification for notes on _meta usage + *

+ * Note: {@code level} and {@code data} are required by the MCP specification. + * Deserialization accepts missing values and substitutes defaults to avoid breaking + * existing integrations that may omit these fields. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -2281,15 +3034,48 @@ public record LoggingMessageNotification( // @formatter:off @JsonProperty("data") String data, @JsonProperty("_meta") Map meta) implements Notification { // @formatter:on + public LoggingMessageNotification { + Assert.notNull(level, "level must not be null"); + Assert.notNull(data, "data must not be null"); + } + + @JsonCreator + static LoggingMessageNotification fromJson(@JsonProperty("level") LoggingLevel level, + @JsonProperty("logger") String loggerName, @JsonProperty("data") String data, + @JsonProperty("_meta") Map meta) { + if (level == null || data == null) { + List missing = new ArrayList<>(); + if (level == null) { + missing.add("level -> INFO"); + level = LoggingLevel.INFO; + } + if (data == null) { + missing.add("data -> ''"); + data = ""; + } + McpSchema.logger.warn("LoggingMessageNotification: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new LoggingMessageNotification(level, loggerName, data, meta); + } + // backwards compatibility constructor public LoggingMessageNotification(LoggingLevel level, String logger, String data) { this(level, logger, data, null); } + /** + * @deprecated Use {@link #builder(LoggingLevel, String)} instead. + */ + @Deprecated public static Builder builder() { return new Builder(); } + public static Builder builder(LoggingLevel level, String data) { + return new Builder(level, data); + } + public static class Builder { private LoggingLevel level = LoggingLevel.INFO; @@ -2300,7 +3086,24 @@ public static class Builder { private Map meta; + /** + * @deprecated Use + * {@link LoggingMessageNotification#builder(LoggingLevel, String)} factory + * method instead. + */ + @Deprecated + public Builder() { + } + + private Builder(LoggingLevel level, String data) { + Assert.notNull(level, "level must not be null"); + Assert.notNull(data, "data must not be null"); + this.level = level; + this.data = data; + } + public Builder level(LoggingLevel level) { + Assert.notNull(level, "level must not be null"); this.level = level; return this; } @@ -2311,6 +3114,7 @@ public Builder logger(String logger) { } public Builder data(String data) { + Assert.notNull(data, "data must not be null"); this.data = data; return this; } @@ -2321,6 +3125,7 @@ public Builder meta(Map meta) { } public LoggingMessageNotification build() { + Assert.notNull(data, "data must not be null"); return new LoggingMessageNotification(level, logger, data, meta); } @@ -2382,6 +3187,20 @@ public static LoggingLevel fromValue(String value) { @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) public record SetLevelRequest(@JsonProperty("level") LoggingLevel level) { + + public SetLevelRequest { + Assert.notNull(level, "level must not be null"); + } + + @JsonCreator + static SetLevelRequest fromJson(@JsonProperty("level") LoggingLevel level) { + if (level == null) { + logger.warn( + "SetLevelRequest: missing required field 'level' during deserialization, using default 'info'"); + level = LoggingLevel.INFO; + } + return new SetLevelRequest(level); + } } // --------------------------- @@ -2491,6 +3310,18 @@ public record CompleteRequest( // @formatter:off @JsonProperty("_meta") Map meta, @JsonProperty("context") CompleteContext context) implements Request { // @formatter:on + public CompleteRequest { + Assert.notNull(ref, "ref must not be null"); + Assert.notNull(argument, "argument must not be null"); + } + + @JsonCreator + static CompleteRequest fromJson(@JsonProperty("ref") McpSchema.CompleteReference ref, + @JsonProperty("argument") CompleteArgument argument, @JsonProperty("_meta") Map meta, + @JsonProperty("context") CompleteContext context) { + return new CompleteRequest(ref, argument, meta, context); + } + public CompleteRequest(McpSchema.CompleteReference ref, CompleteArgument argument, Map meta) { this(ref, argument, meta, null); } @@ -2537,6 +3368,21 @@ public record CompleteResult(// @formatter:off @JsonProperty("completion") CompleteCompletion completion, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + public CompleteResult { + Assert.notNull(completion, "completion must not be null"); + } + + @JsonCreator + static CompleteResult fromJson(@JsonProperty("completion") CompleteCompletion completion, + @JsonProperty("_meta") Map meta) { + if (completion == null) { + logger.warn( + "CompleteResult: missing required field 'completion' during deserialization, using default {values=[]}"); + completion = new CompleteCompletion(List.of(), null, null); + } + return new CompleteResult(completion, meta); + } + // backwards compatibility constructor public CompleteResult(CompleteCompletion completion) { this(completion, null); @@ -2616,6 +3462,20 @@ public record TextContent( // @formatter:off @JsonProperty("text") String text, @JsonProperty("_meta") Map meta) implements Annotated, Content { // @formatter:on + public TextContent { + Assert.notNull(text, "text must not be null"); + } + + @JsonCreator + static TextContent fromJson(@JsonProperty("annotations") Annotations annotations, + @JsonProperty("text") String text, @JsonProperty("_meta") Map meta) { + if (text == null) { + logger.warn("TextContent: missing required field 'text' during deserialization, using default ''"); + text = ""; + } + return new TextContent(annotations, text, meta); + } + public TextContent(Annotations annotations, String text) { this(annotations, text, null); } @@ -2642,6 +3502,31 @@ public record ImageContent( // @formatter:off @JsonProperty("mimeType") String mimeType, @JsonProperty("_meta") Map meta) implements Annotated, Content { // @formatter:on + public ImageContent { + Assert.notNull(data, "data must not be null"); + Assert.notNull(mimeType, "mimeType must not be null"); + } + + @JsonCreator + static ImageContent fromJson(@JsonProperty("annotations") Annotations annotations, + @JsonProperty("data") String data, @JsonProperty("mimeType") String mimeType, + @JsonProperty("_meta") Map meta) { + if (data == null || mimeType == null) { + List missing = new ArrayList<>(); + if (data == null) { + missing.add("data -> ''"); + data = ""; + } + if (mimeType == null) { + missing.add("mimeType -> ''"); + mimeType = ""; + } + logger.warn("ImageContent: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new ImageContent(annotations, data, mimeType, meta); + } + public ImageContent(Annotations annotations, String data, String mimeType) { this(annotations, data, mimeType, null); } @@ -2664,6 +3549,31 @@ public record AudioContent( // @formatter:off @JsonProperty("mimeType") String mimeType, @JsonProperty("_meta") Map meta) implements Annotated, Content { // @formatter:on + public AudioContent { + Assert.notNull(data, "data must not be null"); + Assert.notNull(mimeType, "mimeType must not be null"); + } + + @JsonCreator + static AudioContent fromJson(@JsonProperty("annotations") Annotations annotations, + @JsonProperty("data") String data, @JsonProperty("mimeType") String mimeType, + @JsonProperty("_meta") Map meta) { + if (data == null || mimeType == null) { + List missing = new ArrayList<>(); + if (data == null) { + missing.add("data -> ''"); + data = ""; + } + if (mimeType == null) { + missing.add("mimeType -> ''"); + mimeType = ""; + } + logger.warn("AudioContent: missing required fields during deserialization: {}", + String.join(", ", missing)); + } + return new AudioContent(annotations, data, mimeType, meta); + } + // backwards compatibility constructor public AudioContent(Annotations annotations, String data, String mimeType) { this(annotations, data, mimeType, null); @@ -2687,6 +3597,21 @@ public record EmbeddedResource( // @formatter:off @JsonProperty("resource") ResourceContents resource, @JsonProperty("_meta") Map meta) implements Annotated, Content { // @formatter:on + public EmbeddedResource { + Assert.notNull(resource, "resource must not be null"); + } + + @JsonCreator + static EmbeddedResource fromJson(@JsonProperty("annotations") Annotations annotations, + @JsonProperty("resource") ResourceContents resource, @JsonProperty("_meta") Map meta) { + if (resource == null) { + logger.warn( + "EmbeddedResource: missing required field 'resource' during deserialization, using empty text resource"); + resource = new TextResourceContents("", null, ""); + } + return new EmbeddedResource(annotations, resource, meta); + } + // backwards compatibility constructor public EmbeddedResource(Annotations annotations, ResourceContents resource) { this(annotations, resource, null); @@ -2816,6 +3741,20 @@ public record Root( // @formatter:off @JsonProperty("name") String name, @JsonProperty("_meta") Map meta) { // @formatter:on + public Root { + Assert.notNull(uri, "uri must not be null"); + } + + @JsonCreator + static Root fromJson(@JsonProperty("uri") String uri, @JsonProperty("name") String name, + @JsonProperty("_meta") Map meta) { + if (uri == null) { + logger.warn("Root: missing required field 'uri' during deserialization, using default ''"); + uri = ""; + } + return new Root(uri, name, meta); + } + public Root(String uri, String name) { this(uri, name, null); } @@ -2840,6 +3779,20 @@ public record ListRootsResult( // @formatter:off @JsonProperty("nextCursor") String nextCursor, @JsonProperty("_meta") Map meta) implements Result { // @formatter:on + public ListRootsResult { + Assert.notNull(roots, "roots must not be null"); + } + + @JsonCreator + static ListRootsResult fromJson(@JsonProperty("roots") List roots, + @JsonProperty("nextCursor") String nextCursor, @JsonProperty("_meta") Map meta) { + if (roots == null) { + logger.warn("ListRootsResult: missing required field 'roots' during deserialization, using default []"); + roots = List.of(); + } + return new ListRootsResult(roots, nextCursor, meta); + } + public ListRootsResult(List roots) { this(roots, null); } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java b/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java index d3db7fb4b..d85544074 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/util/ToolInputValidator.java @@ -36,7 +36,7 @@ private ToolInputValidator() { */ public static CallToolResult validate(McpSchema.Tool tool, Map arguments, boolean validateToolInputs, JsonSchemaValidator validator) { - if (!validateToolInputs || tool.inputSchema() == null || validator == null) { + if (!validateToolInputs || tool.inputSchema() == null || tool.inputSchema().isEmpty() || validator == null) { return null; } Map args = arguments != null ? arguments : Map.of(); diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java index ee8c70ffe..ba283dd86 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/AsyncToolSpecificationBuilderTest.java @@ -193,7 +193,7 @@ void tearDown() { @Test void defaultShouldThrowOnInvalidName() { - Tool invalidTool = Tool.builder().name("invalid tool name").build(); + Tool invalidTool = Tool.builder().name("invalid tool name").inputSchema(EMPTY_JSON_SCHEMA).build(); assertThatThrownBy( () -> McpServer.async(transportProvider).toolCall(invalidTool, (exchange, request) -> null)) @@ -204,7 +204,7 @@ void defaultShouldThrowOnInvalidName() { @Test void lenientDefaultShouldLogOnInvalidName() { System.setProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY, "false"); - Tool invalidTool = Tool.builder().name("invalid tool name").build(); + Tool invalidTool = Tool.builder().name("invalid tool name").inputSchema(EMPTY_JSON_SCHEMA).build(); assertThatCode(() -> McpServer.async(transportProvider).toolCall(invalidTool, (exchange, request) -> null)) .doesNotThrowAnyException(); @@ -213,7 +213,7 @@ void lenientDefaultShouldLogOnInvalidName() { @Test void lenientConfigurationShouldLogOnInvalidName() { - Tool invalidTool = Tool.builder().name("invalid tool name").build(); + Tool invalidTool = Tool.builder().name("invalid tool name").inputSchema(EMPTY_JSON_SCHEMA).build(); assertThatCode(() -> McpServer.async(transportProvider) .strictToolNameValidation(false) @@ -224,7 +224,7 @@ void lenientConfigurationShouldLogOnInvalidName() { @Test void serverConfigurationShouldOverrideDefault() { System.setProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY, "false"); - Tool invalidTool = Tool.builder().name("invalid tool name").build(); + Tool invalidTool = Tool.builder().name("invalid tool name").inputSchema(EMPTY_JSON_SCHEMA).build(); assertThatThrownBy(() -> McpServer.async(transportProvider) .strictToolNameValidation(true) diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java index e6161a59f..5f07b318c 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java @@ -227,10 +227,9 @@ void testSetMinLoggingLevelWithNullValue() { @Test void testLoggingNotificationWithAllowedLevel() { - McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.ERROR) + McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.ERROR, "Test error message") .logger("test-logger") - .data("Test error message") .build(); when(mockSession.isNotificationForLevelAllowed(any())).thenReturn(Boolean.TRUE); @@ -248,10 +247,9 @@ void testLoggingNotificationWithFilteredLevel() { exchange.setMinLoggingLevel(McpSchema.LoggingLevel.DEBUG); verify(mockSession, times(1)).setMinLoggingLevel(eq(McpSchema.LoggingLevel.DEBUG)); - McpSchema.LoggingMessageNotification debugNotification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.DEBUG) + McpSchema.LoggingMessageNotification debugNotification = McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.DEBUG, "Debug message that should be filtered") .logger("test-logger") - .data("Debug message that should be filtered") .build(); when(mockSession.isNotificationForLevelAllowed(eq(McpSchema.LoggingLevel.DEBUG))).thenReturn(Boolean.TRUE); @@ -264,10 +262,9 @@ void testLoggingNotificationWithFilteredLevel() { verify(mockSession, times(1)).sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(debugNotification)); - McpSchema.LoggingMessageNotification warningNotification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.WARNING) + McpSchema.LoggingMessageNotification warningNotification = McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.WARNING, "Debug message that should be filtered") .logger("test-logger") - .data("Debug message that should be filtered") .build(); StepVerifier.create(exchange.loggingNotification(warningNotification)).verifyComplete(); @@ -279,10 +276,9 @@ void testLoggingNotificationWithFilteredLevel() { @Test void testLoggingNotificationWithSessionError() { - McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.ERROR) + McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.ERROR, "Test error message") .logger("test-logger") - .data("Test error message") .build(); when(mockSession.isNotificationForLevelAllowed(any())).thenReturn(Boolean.TRUE); @@ -304,8 +300,8 @@ void testCreateElicitationWithNullCapabilities() { McpAsyncServerExchange exchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId", mockSession, null, clientInfo, McpTransportContext.EMPTY); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide your name") + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide your name", Map.of("type", "object")) .build(); StepVerifier.create(exchangeWithNullCapabilities.createElicitation(elicitRequest)) @@ -328,8 +324,8 @@ void testCreateElicitationWithoutElicitationCapabilities() { McpAsyncServerExchange exchangeWithoutElicitation = new McpAsyncServerExchange("testSessionId", mockSession, capabilitiesWithoutElicitation, clientInfo, McpTransportContext.EMPTY); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide your name") + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide your name", Map.of("type", "object")) .build(); StepVerifier.create(exchangeWithoutElicitation.createElicitation(elicitRequest)).verifyErrorSatisfies(error -> { @@ -359,9 +355,8 @@ void testCreateElicitationWithComplexRequest() { java.util.Map.of("type", "number"))); requestedSchema.put("required", java.util.List.of("name")); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide your personal information") - .requestedSchema(requestedSchema) + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide your personal information", requestedSchema) .build(); java.util.Map responseContent = new java.util.HashMap<>(); @@ -395,8 +390,8 @@ void testCreateElicitationWithDeclineAction() { McpAsyncServerExchange exchangeWithElicitation = new McpAsyncServerExchange("testSessionId", mockSession, capabilitiesWithElicitation, clientInfo, McpTransportContext.EMPTY); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide sensitive information") + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide sensitive information", Map.of("type", "object")) .build(); McpSchema.ElicitResult expectedResult = McpSchema.ElicitResult.builder() @@ -422,8 +417,8 @@ void testCreateElicitationWithCancelAction() { McpAsyncServerExchange exchangeWithElicitation = new McpAsyncServerExchange("testSessionId", mockSession, capabilitiesWithElicitation, clientInfo, McpTransportContext.EMPTY); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide your information") + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide your information", Map.of("type", "object")) .build(); McpSchema.ElicitResult expectedResult = McpSchema.ElicitResult.builder() @@ -449,8 +444,8 @@ void testCreateElicitationWithSessionError() { McpAsyncServerExchange exchangeWithElicitation = new McpAsyncServerExchange("testSessionId", mockSession, capabilitiesWithElicitation, clientInfo, McpTransportContext.EMPTY); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide your name") + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide your name", Map.of("type", "object")) .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), any(TypeRef.class))) @@ -471,9 +466,10 @@ void testCreateMessageWithNullCapabilities() { McpAsyncServerExchange exchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId", mockSession, null, clientInfo, McpTransportContext.EMPTY); - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays - .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder(Arrays + .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!"))), + 1000) .build(); StepVerifier.create(exchangeWithNullCapabilities.createMessage(createMessageRequest)) @@ -497,9 +493,10 @@ void testCreateMessageWithoutSamplingCapabilities() { McpAsyncServerExchange exchangeWithoutSampling = new McpAsyncServerExchange("testSessionId", mockSession, capabilitiesWithoutSampling, clientInfo, McpTransportContext.EMPTY); - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays - .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder(Arrays + .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!"))), + 1000) .build(); StepVerifier.create(exchangeWithoutSampling.createMessage(createMessageRequest)).verifyErrorSatisfies(error -> { @@ -522,9 +519,10 @@ void testCreateMessageWithBasicRequest() { McpAsyncServerExchange exchangeWithSampling = new McpAsyncServerExchange("testSessionId", mockSession, capabilitiesWithSampling, clientInfo, McpTransportContext.EMPTY); - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays - .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder(Arrays + .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!"))), + 1000) .build(); McpSchema.CreateMessageResult expectedResult = McpSchema.CreateMessageResult.builder() @@ -559,10 +557,13 @@ void testCreateMessageWithImageContent() { capabilitiesWithSampling, clientInfo, McpTransportContext.EMPTY); // Create request with image content - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.ImageContent(null, "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...", - "image/jpeg")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder( + Arrays + .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.ImageContent(null, + "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...", "image/jpeg"))), + 1000) .build(); McpSchema.CreateMessageResult expectedResult = McpSchema.CreateMessageResult.builder() @@ -593,9 +594,11 @@ void testCreateMessageWithSessionError() { McpAsyncServerExchange exchangeWithSampling = new McpAsyncServerExchange("testSessionId", mockSession, capabilitiesWithSampling, clientInfo, McpTransportContext.EMPTY); - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays - .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder( + Arrays + .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello"))), + 1000) .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest), @@ -617,9 +620,9 @@ void testCreateMessageWithIncludeContext() { McpAsyncServerExchange exchangeWithSampling = new McpAsyncServerExchange("testSessionId", mockSession, capabilitiesWithSampling, clientInfo, McpTransportContext.EMPTY); - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("What files are available?")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder(Arrays.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("What files are available?"))), 1000) .includeContext(McpSchema.CreateMessageRequest.ContextInclusionStrategy.ALL_SERVERS) .build(); diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java index fba733c9a..009a17955 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java @@ -221,10 +221,9 @@ void testLoggingNotificationWithNullMessage() { @Test void testLoggingNotificationWithAllowedLevel() { - McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.ERROR) + McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.ERROR, "Test error message") .logger("test-logger") - .data("Test error message") .build(); when(mockSession.isNotificationForLevelAllowed(any())).thenReturn(Boolean.TRUE); @@ -243,10 +242,9 @@ void testLoggingNotificationWithFilteredLevel() { asyncExchange.setMinLoggingLevel(McpSchema.LoggingLevel.DEBUG); verify(mockSession, times(1)).setMinLoggingLevel(McpSchema.LoggingLevel.DEBUG); - McpSchema.LoggingMessageNotification debugNotification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.DEBUG) + McpSchema.LoggingMessageNotification debugNotification = McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.DEBUG, "Debug message that should be filtered") .logger("test-logger") - .data("Debug message that should be filtered") .build(); when(mockSession.isNotificationForLevelAllowed(eq(McpSchema.LoggingLevel.DEBUG))).thenReturn(Boolean.TRUE); @@ -259,10 +257,9 @@ void testLoggingNotificationWithFilteredLevel() { verify(mockSession, times(1)).sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(debugNotification)); - McpSchema.LoggingMessageNotification warningNotification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.WARNING) + McpSchema.LoggingMessageNotification warningNotification = McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.WARNING, "Debug message that should be filtered") .logger("test-logger") - .data("Debug message that should be filtered") .build(); exchange.loggingNotification(warningNotification); @@ -275,10 +272,9 @@ void testLoggingNotificationWithFilteredLevel() { @Test void testLoggingNotificationWithSessionError() { - McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.ERROR) + McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.ERROR, "Test error message") .logger("test-logger") - .data("Test error message") .build(); when(mockSession.isNotificationForLevelAllowed(any())).thenReturn(Boolean.TRUE); @@ -301,8 +297,8 @@ void testCreateElicitationWithNullCapabilities() { McpSyncServerExchange exchangeWithNullCapabilities = new McpSyncServerExchange( asyncExchangeWithNullCapabilities); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide your name") + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide your name", Map.of("type", "object")) .build(); assertThatThrownBy(() -> exchangeWithNullCapabilities.createElicitation(elicitRequest)) @@ -324,8 +320,8 @@ void testCreateElicitationWithoutElicitationCapabilities() { mockSession, capabilitiesWithoutElicitation, clientInfo, McpTransportContext.EMPTY); McpSyncServerExchange exchangeWithoutElicitation = new McpSyncServerExchange(asyncExchangeWithoutElicitation); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide your name") + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide your name", Map.of("type", "object")) .build(); assertThatThrownBy(() -> exchangeWithoutElicitation.createElicitation(elicitRequest)) @@ -355,9 +351,8 @@ void testCreateElicitationWithComplexRequest() { java.util.Map.of("type", "number"))); requestedSchema.put("required", java.util.List.of("name")); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide your personal information") - .requestedSchema(requestedSchema) + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide your personal information", requestedSchema) .build(); java.util.Map responseContent = new java.util.HashMap<>(); @@ -392,8 +387,8 @@ void testCreateElicitationWithDeclineAction() { capabilitiesWithElicitation, clientInfo, McpTransportContext.EMPTY); McpSyncServerExchange exchangeWithElicitation = new McpSyncServerExchange(asyncExchangeWithElicitation); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide sensitive information") + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide sensitive information", Map.of("type", "object")) .build(); McpSchema.ElicitResult expectedResult = McpSchema.ElicitResult.builder() @@ -420,8 +415,8 @@ void testCreateElicitationWithCancelAction() { capabilitiesWithElicitation, clientInfo, McpTransportContext.EMPTY); McpSyncServerExchange exchangeWithElicitation = new McpSyncServerExchange(asyncExchangeWithElicitation); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide your information") + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide your information", Map.of("type", "object")) .build(); McpSchema.ElicitResult expectedResult = McpSchema.ElicitResult.builder() @@ -448,8 +443,8 @@ void testCreateElicitationWithSessionError() { capabilitiesWithElicitation, clientInfo, McpTransportContext.EMPTY); McpSyncServerExchange exchangeWithElicitation = new McpSyncServerExchange(asyncExchangeWithElicitation); - McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder() - .message("Please provide your name") + McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest + .builder("Please provide your name", Map.of("type", "object")) .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), any(TypeRef.class))) @@ -472,9 +467,10 @@ void testCreateMessageWithNullCapabilities() { McpSyncServerExchange exchangeWithNullCapabilities = new McpSyncServerExchange( asyncExchangeWithNullCapabilities); - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays - .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder(Arrays + .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!"))), + 1000) .build(); assertThatThrownBy(() -> exchangeWithNullCapabilities.createMessage(createMessageRequest)) @@ -497,9 +493,10 @@ void testCreateMessageWithoutSamplingCapabilities() { capabilitiesWithoutSampling, clientInfo, McpTransportContext.EMPTY); McpSyncServerExchange exchangeWithoutSampling = new McpSyncServerExchange(asyncExchangeWithoutSampling); - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays - .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder(Arrays + .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!"))), + 1000) .build(); assertThatThrownBy(() -> exchangeWithoutSampling.createMessage(createMessageRequest)) @@ -522,9 +519,10 @@ void testCreateMessageWithBasicRequest() { capabilitiesWithSampling, clientInfo, McpTransportContext.EMPTY); McpSyncServerExchange exchangeWithSampling = new McpSyncServerExchange(asyncExchangeWithSampling); - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays - .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder(Arrays + .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!"))), + 1000) .build(); McpSchema.CreateMessageResult expectedResult = McpSchema.CreateMessageResult.builder() @@ -560,10 +558,13 @@ void testCreateMessageWithImageContent() { McpSyncServerExchange exchangeWithSampling = new McpSyncServerExchange(asyncExchangeWithSampling); // Create request with image content - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.ImageContent(null, "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...", - "image/jpeg")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder( + Arrays + .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.ImageContent(null, + "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...", "image/jpeg"))), + 1000) .build(); McpSchema.CreateMessageResult expectedResult = McpSchema.CreateMessageResult.builder() @@ -595,9 +596,11 @@ void testCreateMessageWithSessionError() { capabilitiesWithSampling, clientInfo, McpTransportContext.EMPTY); McpSyncServerExchange exchangeWithSampling = new McpSyncServerExchange(asyncExchangeWithSampling); - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays - .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder( + Arrays + .asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello"))), + 1000) .build(); when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest), @@ -620,9 +623,9 @@ void testCreateMessageWithIncludeContext() { capabilitiesWithSampling, clientInfo, McpTransportContext.EMPTY); McpSyncServerExchange exchangeWithSampling = new McpSyncServerExchange(asyncExchangeWithSampling); - McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(Arrays.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("What files are available?")))) + McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest + .builder(Arrays.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("What files are available?"))), 1000) .includeContext(McpSchema.CreateMessageRequest.ContextInclusionStrategy.ALL_SERVERS) .build(); diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java index f7364be2d..56f182de2 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/SyncToolSpecificationBuilderTest.java @@ -138,7 +138,7 @@ void tearDown() { @Test void defaultShouldThrowOnInvalidName() { - Tool invalidTool = Tool.builder().name("invalid tool name").build(); + Tool invalidTool = Tool.builder().name("invalid tool name").inputSchema(EMPTY_JSON_SCHEMA).build(); assertThatThrownBy( () -> McpServer.sync(transportProvider).toolCall(invalidTool, (exchange, request) -> null)) @@ -149,7 +149,7 @@ void defaultShouldThrowOnInvalidName() { @Test void lenientDefaultShouldLogOnInvalidName() { System.setProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY, "false"); - Tool invalidTool = Tool.builder().name("invalid tool name").build(); + Tool invalidTool = Tool.builder().name("invalid tool name").inputSchema(EMPTY_JSON_SCHEMA).build(); assertThatCode(() -> McpServer.sync(transportProvider).toolCall(invalidTool, (exchange, request) -> null)) .doesNotThrowAnyException(); @@ -158,7 +158,7 @@ void lenientDefaultShouldLogOnInvalidName() { @Test void lenientConfigurationShouldLogOnInvalidName() { - Tool invalidTool = Tool.builder().name("invalid tool name").build(); + Tool invalidTool = Tool.builder().name("invalid tool name").inputSchema(EMPTY_JSON_SCHEMA).build(); assertThatCode(() -> McpServer.sync(transportProvider) .strictToolNameValidation(false) @@ -169,7 +169,7 @@ void lenientConfigurationShouldLogOnInvalidName() { @Test void serverConfigurationShouldOverrideDefault() { System.setProperty(ToolNameValidator.STRICT_VALIDATION_PROPERTY, "false"); - Tool invalidTool = Tool.builder().name("invalid tool name").build(); + Tool invalidTool = Tool.builder().name("invalid tool name").inputSchema(EMPTY_JSON_SCHEMA).build(); assertThatThrownBy(() -> McpServer.sync(transportProvider) .strictToolNameValidation(true) diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/util/ToolInputValidatorTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/util/ToolInputValidatorTests.java index 4d073d1a7..f845054de 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/util/ToolInputValidatorTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/util/ToolInputValidatorTests.java @@ -16,10 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; /** * Tests for {@link ToolInputValidator}. @@ -39,7 +36,10 @@ class ToolInputValidatorTests { .inputSchema(inputSchema) .build(); - private final Tool toolWithoutSchema = Tool.builder().name("test-tool").description("Test tool").build(); + private final Tool toolWithoutSchema = Tool.builder() + .name("test-tool") + .description("Test tool") + .build(); @Test void validate_whenDisabled_returnsNull() { @@ -51,10 +51,12 @@ void validate_whenDisabled_returnsNull() { @Test void validate_whenNoSchema_returnsNull() { + when(validator.validate(any(), any())).thenReturn(ValidationResponse.asValid(null)); + CallToolResult result = ToolInputValidator.validate(toolWithoutSchema, Map.of("name", "test"), true, validator); assertThat(result).isNull(); - verify(validator, never()).validate(any(), any()); + verify(validator).validate(any(), any()); } @Test diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java index beec006ba..7675f4e36 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java @@ -162,9 +162,9 @@ void testCreateMessageSuccess(String clientType) { .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { - var createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) + var createMessageRequest = McpSchema.CreateMessageRequest + .builder(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message"))), 1000) .modelPreferences(ModelPreferences.builder() .hints(List.of()) .costPriority(1.0) @@ -241,9 +241,9 @@ void testCreateMessageWithRequestTimeoutSuccess(String clientType) throws Interr .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { - var createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) + var createMessageRequest = McpSchema.CreateMessageRequest + .builder(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message"))), 1000) .modelPreferences(ModelPreferences.builder() .hints(List.of()) .costPriority(1.0) @@ -316,9 +316,9 @@ void testCreateMessageWithRequestTimeoutFail(String clientType) throws Interrupt .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { - var createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) + var createMessageRequest = McpSchema.CreateMessageRequest + .builder(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Test message"))), 1000) .modelPreferences(ModelPreferences.builder() .hints(List.of()) .costPriority(1.0) @@ -412,9 +412,8 @@ void testCreateElicitationSuccess(String clientType) { .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { - var elicitationRequest = McpSchema.ElicitRequest.builder() - .message("Test message") - .requestedSchema( + var elicitationRequest = McpSchema.ElicitRequest + .builder("Test message", Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) .build(); @@ -471,9 +470,8 @@ void testCreateElicitationWithRequestTimeoutSuccess(String clientType) { .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { - var elicitationRequest = McpSchema.ElicitRequest.builder() - .message("Test message") - .requestedSchema( + var elicitationRequest = McpSchema.ElicitRequest + .builder("Test message", Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) .build(); @@ -542,9 +540,8 @@ void testCreateElicitationWithRequestTimeoutFail(String clientType) { .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) .callHandler((exchange, request) -> { - var elicitationRequest = ElicitRequest.builder() - .message("Test message") - .requestedSchema( + var elicitationRequest = ElicitRequest + .builder("Test message", Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) .build(); @@ -1113,34 +1110,29 @@ void testLoggingNotification(String clientType) throws InterruptedException { //@formatter:off return exchange // This should be filtered out (DEBUG < NOTICE) - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.DEBUG) + .loggingNotification(McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.DEBUG, "Debug message") .logger("test-logger") - .data("Debug message") .build()) .then(exchange // This should be sent (NOTICE >= NOTICE) - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.NOTICE) + .loggingNotification(McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.NOTICE, "Notice message") .logger("test-logger") - .data("Notice message") .build())) .then(exchange // This should be sent (ERROR > NOTICE) - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.ERROR) + .loggingNotification(McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.ERROR, "Error message") .logger("test-logger") - .data("Error message") .build())) .then(exchange // This should be filtered out (INFO < NOTICE) - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.INFO) + .loggingNotification(McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.INFO, "Another info message") .logger("test-logger") - .data("Another info message") .build())) .then(exchange // This should be sent (ERROR >= NOTICE) - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.ERROR) + .loggingNotification(McpSchema.LoggingMessageNotification + .builder(McpSchema.LoggingLevel.ERROR, "Another error message") .logger("test-logger") - .data("Another error message") .build())) .thenReturn(CallToolResult.builder() .content(List.of(new McpSchema.TextContent("Logging test completed"))) diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java index 47a229afd..3692c1c4e 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java @@ -417,9 +417,9 @@ void testElicitationCreateRequestHandling() { assertThat(asyncMcpClient.initialize().block()).isNotNull(); // Create a mock elicitation - var elicitRequest = McpSchema.ElicitRequest.builder() - .message("Test message") - .requestedSchema(Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) + var elicitRequest = McpSchema.ElicitRequest + .builder("Test message", + Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) .build(); // Simulate incoming request @@ -462,9 +462,9 @@ void testElicitationFailRequestHandling(McpSchema.ElicitResult.Action action) { assertThat(asyncMcpClient.initialize().block()).isNotNull(); // Create a mock elicitation - var elicitRequest = McpSchema.ElicitRequest.builder() - .message("Test message") - .requestedSchema(Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) + var elicitRequest = McpSchema.ElicitRequest + .builder("Test message", + Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) .build(); // Simulate incoming request diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java index 3d40453a3..4a18fa1cd 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java @@ -184,10 +184,10 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) { true // hasMore )); - AtomicReference samplingRequest = new AtomicReference<>(); + AtomicReference completeRequest = new AtomicReference<>(); BiFunction completionHandler = (transportContext, request) -> { - samplingRequest.set(request); + completeRequest.set(request); return completionResponse; }; @@ -214,9 +214,9 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) { assertThat(result).isNotNull(); - assertThat(samplingRequest.get().argument().name()).isEqualTo("language"); - assertThat(samplingRequest.get().argument().value()).isEqualTo("py"); - assertThat(samplingRequest.get().ref().type()).isEqualTo(PromptReference.TYPE); + assertThat(completeRequest.get().argument().name()).isEqualTo("language"); + assertThat(completeRequest.get().argument().value()).isEqualTo("py"); + assertThat(completeRequest.get().ref().type()).isEqualTo(PromptReference.TYPE); } finally { mcpServer.close(); diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java index 54fb80a78..5a26402c7 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/McpCompletionTests.java @@ -13,11 +13,9 @@ import org.apache.catalina.LifecycleState; import org.apache.catalina.startup.Tomcat; -import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; @@ -37,6 +35,9 @@ import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; import io.modelcontextprotocol.spec.McpError; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * Tests for completion functionality with context support. * @@ -324,4 +325,35 @@ void testCompletionErrorOnMissingContext() { mcpServer.close(); } + @Test + void testPromptWithoutArgumentsCompletionForArgument() { + BiFunction completionHandler = (exchange, + request) -> new CompleteResult(new CompleteResult.CompleteCompletion(List.of("test"), 1, false)); + + McpSchema.Prompt prompt = new Prompt("test-prompt", "this is a test prompt", null); + + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().completions().build()) + .prompts(new McpServerFeatures.SyncPromptSpecification(prompt, + (mcpSyncServerExchange, getPromptRequest) -> null)) + .completions(new McpServerFeatures.SyncCompletionSpecification( + new PromptReference(PromptReference.TYPE, "test-prompt"), completionHandler)) + .build(); + + try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) + .build()) { + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + // try completing an argument knowing that the prompt is not parameterized + CompleteRequest request = new CompleteRequest(new PromptReference(PromptReference.TYPE, "test-prompt"), + new CompleteRequest.CompleteArgument("arg", "val")); + + CompleteResult completeResult = mcpClient.completeCompletion(request); + assertThat(completeResult.completion().values()).isEmpty(); + } + + mcpServer.close(); + } + } \ No newline at end of file diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java index d9f899020..da136b407 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java @@ -26,7 +26,8 @@ class McpServerProtocolVersionTests { private McpSchema.JSONRPCRequest jsonRpcInitializeRequest(String requestId, String protocolVersion) { return new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, McpSchema.METHOD_INITIALIZE, requestId, - new McpSchema.InitializeRequest(protocolVersion, null, CLIENT_INFO)); + new McpSchema.InitializeRequest(protocolVersion, McpSchema.ClientCapabilities.builder().build(), + CLIENT_INFO)); } @Test diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceSubscriptionTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceSubscriptionTests.java index 016e25e9f..63ee14833 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceSubscriptionTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceSubscriptionTests.java @@ -37,8 +37,8 @@ private static McpAsyncServer buildServer(MockMcpServerTransportProvider transpo private static McpSchema.JSONRPCRequest initRequest() { return new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, McpSchema.METHOD_INITIALIZE, - UUID.randomUUID().toString(), - new McpSchema.InitializeRequest(ProtocolVersions.MCP_2025_11_25, null, CLIENT_INFO)); + UUID.randomUUID().toString(), new McpSchema.InitializeRequest(ProtocolVersions.MCP_2025_11_25, + McpSchema.ClientCapabilities.builder().build(), CLIENT_INFO)); } private static McpSchema.JSONRPCNotification initializedNotification() { diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/spec/CompleteReferenceJsonTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/spec/CompleteReferenceJsonTests.java index 1b23c5059..874b995e1 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/spec/CompleteReferenceJsonTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/spec/CompleteReferenceJsonTests.java @@ -7,12 +7,14 @@ import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.io.IOException; import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.json.TypeRef; import org.junit.jupiter.api.Test; +import tools.jackson.databind.exc.ValueInstantiationException; /** * Verifies that {@link McpSchema.CompleteReference} polymorphic dispatch works via direct @@ -82,6 +84,20 @@ void completeRequestConvertValueFromMapDispatchesPromptRef() throws IOException assertThat(req.ref().identifier()).isEqualTo("my-prompt"); } + @Test + void completeRequestMissingRefFailsToInstantiate() throws IOException { + String json = """ + {"argument":{"name":"lang","value":"java"}} + """; + + // This is the real in-process path: params arrives as a Map from JSON-RPC + Object paramsMap = mapper.readValue(json, Object.class); + + assertThatThrownBy(() -> mapper.convertValue(paramsMap, new TypeRef() { + })).isInstanceOf(ValueInstantiationException.class).hasMessageContaining("ref must not be null"); + + } + @Test void typeDiscriminatorAppearsExactlyOnce() throws IOException { McpSchema.PromptReference ref = new McpSchema.PromptReference("p"); diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 09529f2e0..d9e10d082 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -133,13 +133,12 @@ void testCreateMessageRequestWithMeta() throws Exception { Map meta = new HashMap<>(); meta.put("progressToken", "create-message-token-456"); - McpSchema.CreateMessageRequest request = McpSchema.CreateMessageRequest.builder() - .messages(Collections.singletonList(message)) + McpSchema.CreateMessageRequest request = McpSchema.CreateMessageRequest + .builder(Collections.singletonList(message), 1000) .modelPreferences(preferences) .systemPrompt("You are a helpful assistant") .includeContext(McpSchema.CreateMessageRequest.ContextInclusionStrategy.THIS_SERVER) .temperature(0.7) - .maxTokens(1000) .stopSequences(Arrays.asList("STOP", "END")) .metadata(metadata) .meta(meta) @@ -1347,6 +1346,16 @@ void testCallToolResultBuilderWithErrorResult() throws Exception { {"content":[{"type":"text","text":"Error: Operation failed"}],"isError":true}""")); } + @Test + void testCallToolResultDeserializationWithMissingContent() throws Exception { + McpSchema.CallToolResult result = JSON_MAPPER.readValue(""" + {"isError":false}""", McpSchema.CallToolResult.class); + + assertThat(result).isNotNull(); + assertThat(result.content()).isEmpty(); + assertThat(result.isError()).isFalse(); + } + // Sampling Tests @Test @@ -1363,13 +1372,12 @@ void testCreateMessageRequest() throws Exception { Map metadata = new HashMap<>(); metadata.put("session", "test-session"); - McpSchema.CreateMessageRequest request = McpSchema.CreateMessageRequest.builder() - .messages(Collections.singletonList(message)) + McpSchema.CreateMessageRequest request = McpSchema.CreateMessageRequest + .builder(Collections.singletonList(message), 1000) .modelPreferences(preferences) .systemPrompt("You are a helpful assistant") .includeContext(McpSchema.CreateMessageRequest.ContextInclusionStrategy.THIS_SERVER) .temperature(0.7) - .maxTokens(1000) .stopSequences(Arrays.asList("STOP", "END")) .metadata(metadata) .build(); @@ -1384,6 +1392,26 @@ void testCreateMessageRequest() throws Exception { {"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"}}""")); } + @Test + void testSamplingMessageDeserializationWithMissingFields() throws Exception { + McpSchema.SamplingMessage message = JSON_MAPPER.readValue("{}", McpSchema.SamplingMessage.class); + + assertThat(message).isNotNull(); + assertThat(message.role()).isEqualTo(McpSchema.Role.USER); + assertThat(message.content()).isInstanceOf(McpSchema.TextContent.class); + } + + @Test + void testCreateMessageRequestDeserializationWithMissingRequiredFields() throws Exception { + McpSchema.CreateMessageRequest request = JSON_MAPPER.readValue(""" + {"systemPrompt":"hello"}""", McpSchema.CreateMessageRequest.class); + + assertThat(request).isNotNull(); + assertThat(request.messages()).isEmpty(); + assertThat(request.maxTokens()).isZero(); + assertThat(request.systemPrompt()).isEqualTo("hello"); + } + @Test void testCreateMessageResult() throws Exception { McpSchema.TextContent content = new McpSchema.TextContent("Assistant response"); @@ -1426,9 +1454,9 @@ void testCreateMessageResultUnknownStopReason() throws Exception { @Test void testCreateElicitationRequest() throws Exception { - McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder() - .requestedSchema(Map.of("type", "object", "required", List.of("a"), "properties", - Map.of("foo", Map.of("type", "string")))) + McpSchema.ElicitRequest request = McpSchema.ElicitRequest + .builder("Please provide additional information", Map.of("type", "object", "required", List.of("a"), + "properties", Map.of("foo", Map.of("type", "string")))) .build(); String value = JSON_MAPPER.writeValueAsString(request); @@ -1436,8 +1464,9 @@ void testCreateElicitationRequest() throws Exception { assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER) .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) .isObject() - .isEqualTo(json(""" - {"requestedSchema":{"properties":{"foo":{"type":"string"}},"required":["a"],"type":"object"}}""")); + .isEqualTo( + json(""" + {"message":"Please provide additional information","requestedSchema":{"properties":{"foo":{"type":"string"}},"required":["a"],"type":"object"}}""")); } @Test @@ -1456,6 +1485,15 @@ void testCreateElicitationResult() throws Exception { {"action":"accept","content":{"foo":"bar"}}""")); } + @Test + void testElicitRequestDeserializationWithMissingRequiredFields() throws Exception { + McpSchema.ElicitRequest request = JSON_MAPPER.readValue("{}", McpSchema.ElicitRequest.class); + + assertThat(request).isNotNull(); + assertThat(request.message()).isEmpty(); + assertThat(request.requestedSchema()).isEmpty(); + } + @Test void testElicitRequestWithMeta() throws Exception { Map requestedSchema = Map.of("type", "object", "required", List.of("name"), "properties", @@ -1464,9 +1502,7 @@ void testElicitRequestWithMeta() throws Exception { Map meta = new HashMap<>(); meta.put("progressToken", "elicit-token-789"); - McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder() - .message("Please provide your name") - .requestedSchema(requestedSchema) + McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder("Please provide your name", requestedSchema) .meta(meta) .build(); @@ -1755,6 +1791,17 @@ void testProgressNotificationDeserialization() throws Exception { assertThat(notification.meta()).containsEntry("key", "value"); } + @Test + void testProgressNotificationDeserializationWithMissingRequiredFields() throws Exception { + McpSchema.ProgressNotification notification = JSON_MAPPER.readValue(""" + {"total":1.0}""", McpSchema.ProgressNotification.class); + + assertThat(notification).isNotNull(); + assertThat(notification.progressToken()).isEqualTo(""); + assertThat(notification.progress()).isZero(); + assertThat(notification.total()).isEqualTo(1.0); + } + @Test void testProgressNotificationWithoutMessage() throws Exception { McpSchema.ProgressNotification notification = new McpSchema.ProgressNotification("progress-token-789", 0.25, @@ -1768,4 +1815,15 @@ void testProgressNotificationWithoutMessage() throws Exception { {"progressToken":"progress-token-789","progress":0.25}""")); } + @Test + void testLoggingMessageNotificationDeserializationWithMissingRequiredFields() throws Exception { + McpSchema.LoggingMessageNotification notification = JSON_MAPPER.readValue(""" + {"logger":"my-logger"}""", McpSchema.LoggingMessageNotification.class); + + assertThat(notification).isNotNull(); + assertThat(notification.level()).isEqualTo(McpSchema.LoggingLevel.INFO); + assertThat(notification.logger()).isEqualTo("my-logger"); + assertThat(notification.data()).isEmpty(); + } + }