Every wire-serialized record in McpSchema now validates spec-required
fields at construction time. Wire deserialization is intentionally
lenient: a missing required field is replaced with a documented default
and a WARN is logged, instead of failing the parse. A required-first
builder convention is introduced across the schema so it is no longer
possible to obtain a builder that is missing required state.
Required-field guards
---------------------
Every wire record's compact constructor asserts non-null (and non-empty
for String identifiers like name, uri, uriTemplate, version) on its
spec-required components. Passing null now throws
IllegalArgumentException at construction time instead of silently
producing invalid JSON via @JsonInclude(NON_ABSENT). This applies to
the JSON-RPC envelopes, lifecycle types, resource/prompt/tool requests
and results, sampling and elicitation, content records, root, complete,
logging and progress notifications, and the two CompleteReference
implementations. See MIGRATION-2.0.md for the full list.
Lenient wire deserialization
----------------------------
For each of those records (except JSONRPCResponse.JSONRPCError, which
still fails fast) a @JsonCreator static `fromJson` factory substitutes
a documented default for any absent required field — "" for strings,
[] for collections, {} for maps, 0 / 0.0 for numerics, INFO for
LoggingLevel, USER for SamplingMessage.role, ASSISTANT for
CreateMessageResult.role, CANCEL for ElicitResult.action, {values=[]}
for CompleteResult.completion — and logs a WARN naming the field and
the value used. Application code can still observe a malformed message
and react, but the SDK no longer halts the conversation.
Builder convention
------------------
Records that have a builder gain a required-first factory method
`builder(req1, req2, …)`; setters for required fields are removed from
the builder so it cannot be left in an invalid state. Existing no-arg
`builder()` factories and required-field setters are kept where source
compatibility demands it but are marked @Deprecated. New builders are
also added for several records that previously had none (ProgressNotification,
JSONRPCError, CompleteRequest, list/result types, content records, ...).
JSON-RPC envelope ergonomics
----------------------------
Previously every JSON-RPC envelope had to be constructed via the
canonical record constructor and the literal "2.0" string had to be
threaded through every call site, e.g.
new JSONRPCRequest("2.0", "tools/call", id, params)
new JSONRPCResponse("2.0", id, result, null)
new JSONRPCResponse("2.0", id, null, new JSONRPCError(code, message, null))
Now:
new JSONRPCRequest("tools/call", id, params) // jsonrpc defaulted
new JSONRPCNotification("notifications/initialized") // params optional
JSONRPCResponse.result(id, result) // factory
JSONRPCResponse.error(id, new JSONRPCError(code, message)) // 2-arg error
JSONRPCResponse's compact constructor additionally enforces the
JSON-RPC invariant that exactly one of `result` / `error` is set —
previously the SDK could build envelopes that violated the protocol.
CompleteReference changes
-------------------------
- PromptReference.equals/hashCode now key on `name` only (previously
derived from identifier()+type()). Two refs with the same name but
different titles now compare equal — code using PromptReference as a
map/set key should be audited.
- PromptReference's compact constructor pins `type` to "ref/prompt"
and logs a WARN if the caller supplies a different non-null value.
The legacy two-arg `PromptReference(String type, String name)`
constructor remains @Deprecated.
- ResourceReference's record components are reduced from (type, uri)
to (uri) — positional construction breaks. The legacy
`ResourceReference(String type, String uri)` constructor stays
@Deprecated and ignores `type`.
- CompleteReference.identifier() is @Deprecated and now returns null
via a default method on the interface.
Tests / refactor
----------------
Conformance harness, integration tests, sample apps, and internal
callers were migrated to the new builder factories and convenience
constructors. No semantic changes to client/server runtime behaviour
beyond the McpSchema changes above.
Docs
----
- MIGRATION-2.0.md: required-field section now covers the broader
record set and the lenient-deserialization behaviour; PromptReference
WARN behaviour and ResourceReference component reduction are
documented; the JSON-RPC envelope section is rewritten to compare the
pre-2.0 surface with the new one.
- CONTRIBUTING.md: the "Evolving wire-serialized records" recipe is
split into two cases — adding a new optional field (existing rules)
and adding/maintaining a spec-required field (new rules covering
Assert in compact constructor, @JsonCreator fromJson with defaults
and WARN, required-first builder factory).
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
14 KiB
Migration Guide — 2.0
This document covers breaking and behavioural changes introduced in the 2.0 release of the MCP Java SDK.
Jackson / JSON serialization changes
Sealed interfaces removed
The following interfaces were sealed in 1.x and are now plain interfaces in 2.0:
McpSchema.JSONRPCMessageMcpSchema.RequestMcpSchema.ResultMcpSchema.NotificationMcpSchema.ResourceContentsMcpSchema.CompleteReferenceMcpSchema.Content
Impact: Exhaustive switch expressions or switch statements that relied on the sealed hierarchy for completeness checking must add a default branch. The compiler will no longer reject switches that omit one of the known subtypes.
CompleteReference now carries @JsonTypeInfo
CompleteReference (and its implementations PromptReference and ResourceReference) is now annotated with @JsonTypeInfo(use = NAME, include = EXISTING_PROPERTY, property = "type", visible = true). Jackson will automatically dispatch to the correct subtype based on the "type" field in the JSON without any hand-written map-walking code.
Action: Remove any custom code that manually inspected the "type" field of a completion reference map and instantiated PromptReference / ResourceReference by hand. A plain mapper.readValue(json, CompleteRequest.class) or mapper.convertValue(paramsMap, CompleteRequest.class) is sufficient.
Prompt canonical constructor no longer coerces null arguments
In 1.x, new Prompt(name, description, null) silently stored an empty list for arguments. In 2.0 it stores null.
Action:
- Code that expected
prompt.arguments()to return an empty list when not provided will now receivenull. Add a null-check or use the newPrompt.withDefaults(name, description, arguments)factory, which preserves the old behaviour by coercingnullto[]. - On the wire, a prompt without an
argumentsfield deserializes witharguments == null(it is not coerced to an empty list).
CompleteCompletion optional fields omitted when null
CompleteResult.CompleteCompletion.total and CompleteCompletion.hasMore are now omitted from serialized JSON when they are null (previously they were always emitted). Deserializers that required these fields to be present in every response must be updated to treat their absence as null.
CompleteCompletion.values is mandatory in the Java API
The compact constructor for CompleteCompletion asserts that values is not null. Code that constructed a completion result with a null values list will now fail at runtime.
Action: Always pass a non-null list (for example List.of() when there are no suggestions).
LoggingLevel deserialization is lenient
LoggingLevel now uses a @JsonCreator factory (fromValue) so that JSON string values deserialize in a case-insensitive way. Unrecognized level strings deserialize to null instead of causing deserialization to fail.
Impact: SetLevelRequest, LoggingMessageNotification, and any other type that embeds LoggingLevel can observe a null level when the wire value is unknown or misspelled. Downstream code must null-check or validate before use.
Content.type() is ignored for Jackson serialization
The Content interface still exposes type() as a convenience for Java callers, but the method is annotated with @JsonIgnore so Jackson does not treat it as a duplicate "type" property alongside @JsonTypeInfo on the interface.
Impact: Custom serializers or ObjectMapper configuration that relied on serializing Content through the default type() accessor alone should use the concrete content records (each of which carries a real "type" property) or the polymorphic setup on Content.
ServerParameters no longer carries Jackson annotations
ServerParameters (in client/transport) has had its @JsonProperty and @JsonInclude annotations removed. It was never a wire type and is not serialized to JSON in normal SDK usage. If your code serialized or deserialized ServerParameters using Jackson, switch to a plain map or a dedicated DTO.
Record annotation sweep
Wire-oriented public record types in McpSchema consistently use @JsonInclude(JsonInclude.Include.NON_ABSENT) (or equivalent per-type configuration) and @JsonIgnoreProperties(ignoreUnknown = true). Nested capability objects under ClientCapabilities / ServerCapabilities (for example Sampling, Elicitation, CompletionCapabilities, LoggingCapabilities, prompt/resource/tool capability records) also ignore unknown JSON properties. This means:
- Unknown fields in incoming JSON are silently ignored, improving forward compatibility with newer server or client versions.
- Absent optional properties are omitted from outgoing JSON where
NON_ABSENTapplies, and optional Java components deserialize asnullwhen missing on the wire.
Tool.inputSchema is Map<String, Object>, not JsonSchema
The Tool record now models inputSchema (and outputSchema) as arbitrary JSON Schema objects as Map<String, Object>, so dialect-specific keywords ($ref, unevaluatedProperties, vendor extensions, and so on) round-trip without being trimmed by a narrow JsonSchema record.
Impact:
- Java code that used
Tool.inputSchema()as aJsonSchemamust switch toMap<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; preferinputSchema(Map)orinputSchema(McpJsonMapper, String).
Required MCP spec fields are enforced at construction time
Every wire record in McpSchema whose fields are marked required by the MCP spec now asserts non-null (and non-empty for String identifiers like name, uri, uriTemplate, version) in its compact constructor. Passing null throws IllegalArgumentException immediately, instead of producing a structurally invalid object that fails later in serialization or protocol handling.
This applies to (non-exhaustive):
- JSON-RPC envelopes:
JSONRPCRequest,JSONRPCNotification,JSONRPCResponse,JSONRPCResponse.JSONRPCError - Lifecycle:
InitializeRequest,InitializeResult,Implementation - Resources:
Resource,ResourceTemplate,ListResourcesResult,ListResourceTemplatesResult,ReadResourceRequest,ReadResourceResult,SubscribeRequest,UnsubscribeRequest,ResourcesUpdatedNotification,TextResourceContents,BlobResourceContents - Prompts:
Prompt,PromptArgument,PromptMessage,ListPromptsResult,GetPromptRequest,GetPromptResult - Tools:
Tool,ListToolsResult,CallToolRequest,CallToolResult - Sampling / elicitation:
SamplingMessage,CreateMessageRequest,CreateMessageResult,ElicitRequest,ElicitResult - Misc:
ProgressNotification,SetLevelRequest,LoggingMessageNotification,CompleteRequest,CompleteResult,CompleteRequest.CompleteArgument, content records (TextContent,ImageContent,AudioContent,EmbeddedResource),Root,ListRootsResult,PromptReference,ResourceReference
Action: Audit any code that constructs these records with potentially-null values and provide valid, non-null arguments.
Wire deserialization is lenient. Records expose a @JsonCreator fromJson factory that substitutes safe defaults (e.g. [], "", 0, INFO, Action.CANCEL) for any absent required field and logs a WARN naming the field and the substituted value. JSONRPCResponse.JSONRPCError is excluded — malformed JSON-RPC error envelopes still fail immediately.
Note: LoggingMessageNotification/SetLevelRequest default a missing level to INFO, but an unrecognized level string still deserializes to null (see the LoggingLevel section above) and will then fail the canonical constructor. Ensure clients and servers send only recognized level strings.
PromptReference discriminator pinning and equality
PromptReference keeps its (type, name, title) record components, so positional construction from 1.x still compiles. Two behavioural changes:
- The compact constructor pins
typetoref/prompt. Any non-null value other thanref/promptis replaced withref/promptand aWARNis logged. The legacy two-argPromptReference(String type, String name)constructor remains@Deprecatedand routes through the canonical constructor, so it triggers the same WARN. equals/hashCodenow considernameonly (title and type are ignored). Two refs with the same name but different titles compare equal.
Action: Audit any code that used PromptReference as a map key or in a Set — equality semantics changed. If your code constructed instances with a custom type string for testing, switch to PromptReference.builder(name) (or new PromptReference(name)); the WARN tells you which call sites still pass the discriminator.
CompleteReference.identifier() is @Deprecated and now returns null via a default method on the interface.
ResourceReference record component reduced
Components changed from (type, uri) to (uri). Positional construction with two arguments breaks. The legacy ResourceReference(String type, String uri) constructor stays @Deprecated; it ignores type and logs a WARN. Use new ResourceReference(uri) or ResourceReference.builder(uri). The type() accessor still returns ref/resource and Jackson serializes it via @JsonProperty("type") on the accessor.
Builder API: required-first factories; old setters/no-arg builders deprecated
Most records that have a builder have gained a required-first factory method (builder(req1, req2, …)) and the corresponding setters for required fields are removed from the builder. The old no-arg builder() factory and public no-arg Builder() constructor are kept but @Deprecated where they would allow constructing a builder without required state.
Examples:
| Type | Old (deprecated) | New |
|---|---|---|
Resource |
Resource.builder().uri(u).name(n)… |
Resource.builder(uri, name)… |
ResourceTemplate |
ResourceTemplate.builder().uriTemplate(u).name(n)… |
ResourceTemplate.builder(uriTemplate, name)… |
Implementation |
new Implementation(name, version) |
Implementation.builder(name, version)… |
InitializeRequest / InitializeResult |
… .builder()… |
… .builder(protocolVersion, capabilities, clientInfo/serverInfo) |
Tool |
Tool.builder().name(n)… |
Tool.builder(name)… |
Prompt / PromptArgument / GetPromptRequest |
… .builder().name(n)… |
… .builder(name)… |
PromptMessage / SamplingMessage |
… .builder().role(r).content(c)… |
… .builder(role, content)… |
CreateMessageRequest |
… .builder().messages(m).maxTokens(n)… |
… .builder(messages, maxTokens)… |
ElicitRequest |
… .builder().message(m).requestedSchema(s)… |
… .builder(message, requestedSchema)… |
LoggingMessageNotification |
… .builder().level(l).data(d)… |
… .builder(level, data)… |
ListResourcesResult / ListResourceTemplatesResult / ListPromptsResult / ListToolsResult / ListRootsResult |
… .builder()… |
… .builder(items)… |
ReadResourceRequest / SubscribeRequest / UnsubscribeRequest / ResourcesUpdatedNotification / Root |
n/a | … .builder(uri)… |
ReadResourceResult |
n/a | ReadResourceResult.builder(contents)… |
TextResourceContents / BlobResourceContents |
n/a | `… .builder(uri, text |
TextContent / ImageContent / AudioContent / EmbeddedResource |
n/a | … .builder(text | data, mimeType | resource)… |
CallToolResult |
unchanged | also: required-first content set via builder constructor remains optional |
ProgressNotification |
n/a | ProgressNotification.builder(progressToken, progress) |
JSONRPCResponse.JSONRPCError |
n/a | JSONRPCError.builder(code, message) |
CompleteRequest |
n/a | CompleteRequest.builder(ref, argument) |
Annotations |
n/a | Annotations.builder() |
Capabilities (Sampling, Elicitation, Roots, LoggingCapabilities, CompletionCapabilities, prompt/resource/tool capabilities) |
n/a | … .builder()… |
JSON-RPC envelope ergonomics
In 1.x, every envelope was constructed via the canonical record constructor and the literal "2.0" jsonrpc string had to be threaded through every call site:
new JSONRPCRequest("2.0", "tools/call", id, params);
new JSONRPCNotification("2.0", "notifications/initialized", null);
new JSONRPCResponse("2.0", id, result, null);
new JSONRPCResponse("2.0", id, null, new JSONRPCError(code, message, null));
2.0 adds defaulting constructors and static factories so the "2.0" constant and the unused result/error slot disappear from caller code:
new JSONRPCRequest("tools/call", id); // params optional
new JSONRPCRequest("tools/call", id, params);
new JSONRPCNotification("notifications/initialized"); // params optional
new JSONRPCNotification("notifications/initialized", params);
JSONRPCResponse.result(id, result);
JSONRPCResponse.error(id, new JSONRPCError(code, message)); // 2-arg error
JSONRPCResponse's compact constructor additionally enforces the JSON-RPC invariant that exactly one of result / error is set — previously the SDK could build envelopes that violated the protocol.
The 1.x canonical 4-arg constructors continue to compile.
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.
Action: Ensure inputSchema maps are valid for your validator, tighten client arguments, or disable validation with validateToolInputs(false) on the server builder if you must preserve pre-2.0 behaviour.