This guide covers the breaking and behavioural changes introduced in the 2.0 release of the MCP Java SDK, relative to 1.x, and how to update existing code.
The changes fall into these areas:
- Schema construction and required fields — non-null enforcement and the builder API.
- Schema type and shape changes — record component and type changes in
McpSchema. - JSON serialization behaviour — wire-format changes.
- Server-side validation — runtime validation of tool arguments and embedded schemas.
- Transport changes — removed methods and the SSE deprecation.
- Server API changes — sync server method signature corrections.
- New features — additive, backward-compatible capabilities.
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 stays 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 LoggingLevel deserialization is lenient) and will then fail the canonical constructor. Ensure clients and servers send only recognized level strings.
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. - On the wire, a prompt without an
argumentsfield deserializes witharguments == null(it is not coerced to an empty list).
Most records that have a builder gained a required-first factory method (builder(req1, req2, …)). The old no-arg builder() factory, the public no-arg Builder() constructor, and the setters for the now-required fields are kept but @Deprecated. They still compile, so 1.x code keeps working with deprecation warnings; migrate to the required-first factories to clear them.
| 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).inputSchema(s)… |
Tool.builder(name, inputSchemaMap)… or Tool.builder(name, jsonMapper, inputSchemaJson)… |
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)… |
GetPromptResult |
new GetPromptResult(description, messages) |
GetPromptResult.builder(messages).description(d)… |
TextResourceContents / BlobResourceContents |
n/a | … .builder(uri, text|blob)… |
TextContent / ImageContent / AudioContent / EmbeddedResource |
n/a | … .builder(text | data, mimeType | resource)… |
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()… |
The Tool record now models inputSchema (and outputSchema) as arbitrary JSON Schema objects of type Map<String, Object>, so dialect-specific keywords ($ref, unevaluatedProperties, vendor extensions, and so on) round-trip without being trimmed by a narrow JsonSchema record.
Action:
- 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).
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 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 (and its implementations PromptReference and ResourceReference) is now annotated with @JsonTypeInfo(use = NAME, include = EXISTING_PROPERTY, property = "type", visible = true). Jackson dispatches to the correct subtype based on the "type" field automatically.
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.
CompleteReference.identifier() is @Deprecated and now returns null via a default method on the interface.
PromptReference keeps its (type, name, title) record components, so positional construction from 1.x still compiles, with two behavioural changes:
- The compact constructor pins
typetoref/prompt. Any non-null value other thanref/promptis replaced and 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 you constructed instances with a custom type string, switch to PromptReference.builder(name) (or new PromptReference(name)); the WARN identifies the call sites still passing a discriminator.
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.
To support URL-mode elicitation (see New features), the elicitation request type was split:
ElicitRequestchanged from arecordto aninterface.- The original form-based request record is now
ElicitFormRequest. - The
McpClientbuilderelicitation(...)methods now accept a handler overElicitFormRequestinstead ofElicitRequest.
Action: Replace references to the old ElicitRequest record (construction, instanceof, handler signatures) with ElicitFormRequest. Code that only referred to ElicitRequest as a type continues to compile against the new interface.
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.
Wire-oriented public record types in McpSchema consistently use @JsonInclude(JsonInclude.Include.NON_ABSENT) and @JsonIgnoreProperties(ignoreUnknown = true). Nested capability objects under ClientCapabilities / ServerCapabilities (for example Sampling, Elicitation, CompletionCapabilities, LoggingCapabilities, and the prompt/resource/tool capability records) also ignore unknown JSON properties. As a result:
- 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.
CompleteResult.CompleteCompletion.totalandCompleteCompletion.hasMoreare now omitted from serialized JSON whennull(previously they were always emitted). Deserializers that required these fields to be present must treat their absence asnull.- The compact constructor asserts that
valuesis notnull. Action: always pass a non-null list (for exampleList.of()when there are no suggestions).
LoggingLevel now uses a @JsonCreator factory (fromValue) so JSON string values deserialize case-insensitively. Unrecognized level strings deserialize to null instead of failing.
Impact: SetLevelRequest, LoggingMessageNotification, and any other type embedding LoggingLevel can observe a null level when the wire value is unknown or misspelled. Downstream code must null-check or validate before use.
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 type() accessor alone should use the concrete content records (each of which carries a real "type" property) or the polymorphic setup on Content.
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 errorJSONRPCResponse'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.
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.
The JSON Schema documents that MCP embeds — Tool.inputSchema, Tool.outputSchema, and ElicitRequest.requestedSchema — are now validated against the JSON Schema 2020-12 meta-schema by default. Servers reject malformed schemas at build time (McpServer.build()) and at runtime (addTool()) with an IllegalArgumentException that names the offending field and references SEP-1613. Elicitation requests whose requestedSchema violates the meta-schema are rejected before being sent to the client.
Schemas that explicitly declare a different dialect via $schema are accepted without meta-schema validation — 2020-12 is the default, not the only permitted dialect.
Action: Make embedded schemas valid 2020-12 documents, or set an explicit $schema to opt into a different dialect.
The deprecated Builder.customizeRequest(Consumer<HttpRequest.Builder>) method on HttpClientSseClientTransport and HttpClientStreamableHttpTransport has been removed.
Action: Use requestBuilder(HttpRequest.Builder) for static request setup, or httpRequestCustomizer(McpSyncHttpClientRequestCustomizer) for per-request customization.
The default implementation of protocolVersions() on McpTransport and McpServerTransportProviderBase previously returned only ["2024-11-05"]. It now returns all four versions the SDK understands:
["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"]
Impact for transport implementors: If your custom McpClientTransport or McpServerTransportProvider did not override protocolVersions(), it will now advertise all four versions during protocol negotiation instead of just 2024-11-05. This is the intended upgrade path for most transports, but if you need to restrict your transport to a specific set of versions, override protocolVersions() explicitly and return the desired list.
Impact for users of built-in transports: No action is required. StdioClientTransport, StdioServerTransportProvider, and HttpServletStreamableServerTransportProvider all advertise the full version list.
The HTTP+SSE client and server transports (and their supporting validator/exception types) are deprecated in favour of Streamable HTTP — HttpClientStreamableHttpTransport on the client, and HttpServletStreamableServerTransportProvider on the server. They still work; plan a move to Streamable HTTP.
In 1.x, McpStatelessSyncServer.closeGracefully() accidentally leaked the reactive signature from the underlying async server and returned Mono<Void>. The sync API is intentionally blocking, so returning a Mono was an oversight — callers had to call .block() themselves to get any actual shutdown behaviour.
In 2.0 the return type is corrected to void; the blocking call is performed internally.
Action: Remove any .block() (or .subscribe()) call you had appended to closeGracefully(). The method now blocks until the server has shut down and returns normally.
These are additive and backward-compatible.
Servers can request out-of-band URL input from users (for example payment or API-key flows) during tool execution. Adds ElicitUrlRequest, urlElicitation() / elicitationCompleteConsumer(s)() builder methods on McpClient, sendElicitationComplete() on McpAsyncServer/McpSyncServer, the ElicitationCompleteNotification record, and the URL_ELICITATION_REQUIRED error code. See the ElicitRequest interface change for the related breaking change.
A new opt-in McpClient builder option applyElicitationDefaults(boolean) fills missing keys of an accepted ElicitResult.content with the default values declared in the request's requestedSchema before returning the result to the server. It is a local client config, not a wire capability.
A new Icon record and an optional icons field were added to Implementation, Resource, ResourceTemplate, Prompt, and Tool. Implementation also gains optional description and websiteUrl fields. All fields are optional; existing constructors and builders are unchanged.
The client list operations accept an optional _meta map alongside the pagination cursor: listResources(String cursor, Map<String, Object> meta), listResourceTemplates(...), listPrompts(...), and listTools(...).