Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions MIGRATION-2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object>` (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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,18 +237,14 @@ private static List<McpServerFeatures.SyncToolSpecification> 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)
Expand Down Expand Up @@ -335,9 +331,8 @@ private static List<McpServerFeatures.SyncToolSpecification> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -999,12 +999,9 @@ private McpRequestHandler<McpSchema.CompleteResult> completionCompleteRequestHan
.message("Prompt not found: " + promptReference.name())
.build());
}
if (!promptSpec.prompt()
.arguments()
.stream()
.filter(arg -> arg.name().equals(argumentName))
.findFirst()
.isPresent()) {
List<McpSchema.PromptArgument> 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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -744,12 +744,9 @@ private McpStatelessRequestHandler<McpSchema.CompleteResult> completionCompleteR
.message("Prompt not found: " + promptReference.name())
.build());
}
if (!promptSpec.prompt()
.arguments()
.stream()
.filter(arg -> arg.name().equals(argumentName))
.findFirst()
.isPresent()) {
List<McpSchema.PromptArgument> 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());

Expand Down
Loading
Loading