You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 22a0efa
Browse filesBrowse the repository at this point in the historyBrowse files
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>
Records in `McpSchema` are serialized directly to the MCP JSON wire format. Follow these rules whenever you add a field to an existing record to keep the protocol forward- and backward-compatible.
80
+
Records in `McpSchema` are serialized directly to the MCP JSON wire format. The rules differ depending on whether the field you are adding (or maintaining) is *optional* — Java code may legitimately leave it `null` and the wire may omit it — or *spec-required* by MCP. Follow **Case A** for optional fields and **Case B** for spec-required fields.
81
81
82
-
### Rules
82
+
### Case A — Optional fields
83
83
84
84
1.**Add new components only at the end** of the record's component list. Never reorder or rename existing components.
85
85
2.**Annotate every component** with `@JsonProperty("fieldName")` even when the Java name already matches. This survives local renames via refactoring tools.
86
86
3.**Use boxed types** (`Boolean`, `Integer`, `Long`, `Double`) so the field can be absent on the wire without a special sentinel.
87
-
4.**Default to `null`**, not an empty collection or neutral value, so the `@JsonInclude(NON_NULL)` rule omits the field for clients that don't know about it yet.
87
+
4.**Default to `null`**, not an empty collection or neutral value, so the `@JsonInclude(NON_ABSENT)` rule omits the field for clients that don't know about it yet.
88
88
5.**Keep existing constructors as source-compatible overloads** that delegate to the new canonical constructor and pass `null` for the new component. Do not remove them in the same release that adds the field.
89
-
6.**Do not put `@JsonCreator` on the canonical constructor** unless strictly necessary. Jackson auto-detects record canonical constructors; adding `@JsonCreator` pins deserialization to that exact parameter order forever.
90
-
7.**Do not convert `null` to a default value in the canonical constructor.** Null carries "absent" semantics and must be preserved through the serialization round-trip.
89
+
6.**Do not put `@JsonCreator` on the canonical constructor** unless strictly necessary. Jackson auto-detects record canonical constructors; adding `@JsonCreator` pins deserialization to that exact parameter order forever.*(For records that also have spec-required fields, the `@JsonCreator` belongs on a separate static `fromJson` factory — see Case B, Rule 2.)*
90
+
7.**Do not convert `null` to a default value in the canonical constructor.** Null carries "absent" semantics and must be preserved through the serialization round-trip.*(Spec-required fields are the exception — see Case B, Rule 1.)*
91
91
8.**Add three tests per new field** (put them in the relevant test class in `mcp-test`):
92
92
- Deserialize JSON *without* the field → succeeds, field is `null`.
93
93
- Serialize an instance with the field unset (`null`) → the key is absent from output.
94
94
- Deserialize JSON with an extra *unknown* field → succeeds.
95
-
9.**An inner `Builder` subclass can be used.** This improves the developer experience since frequently not all fields are required.
95
+
9.**An inner `Builder` subclass can be used.** This improves the developer experience since frequently not all fields are required.
96
+
97
+
### Case B — Spec-required fields
98
+
99
+
When the MCP specification marks a field as required, callers must not be able to construct a structurally invalid record, but the wire parser must still tolerate peers that fail to send it. Follow these rules in addition to the relevant Case A rules (annotation, naming, append-only).
100
+
101
+
1.**Reject `null` in the compact constructor.** Use `Assert.notNull` for required objects or `Assert.hasText` for required `String` identifiers (`name`, `uri`, `uriTemplate`, `version`). This throws `IllegalArgumentException` at construction time instead of producing a record that fails later in serialization or protocol handling. Overrides Case A Rule 7 for this field.
102
+
2.**Add a `@JsonCreator` static `fromJson` factory** alongside the canonical constructor. When a required field is absent from the wire, substitute a documented safe default (`""` for strings, `[]` for collections, `{}` for maps, `0` / `0.0` for numerics, `INFO` for `LoggingLevel`, etc.) and log at `WARN` naming the field and the value used. The SDK must not halt the conversation because of a missing field. Place `@JsonCreator` on this `fromJson` factory, never on the canonical constructor (Case A Rule 6 still applies to the canonical constructor itself).
103
+
- Exception: `JSONRPCResponse.JSONRPCError` fails fast on missing `code` / `message` because a malformed JSON-RPC error envelope is unrecoverable.
104
+
3.**Provide a required-first builder factory**`builder(req1, req2, …)` and remove the corresponding setters from the `Builder`. A no-arg `builder()` factory must not exist on a record that has required fields. If one already exists for source compatibility, mark it `@Deprecated`.
105
+
4.**Add tests per required field**:
106
+
- Constructing the record with `null` for the field throws `IllegalArgumentException`.
107
+
- Deserializing JSON *without* the field succeeds and yields the documented default.
108
+
- Deserializing JSON with an extra *unknown* field still succeeds (Case A Rule 8 also applies).
Copy file name to clipboardExpand all lines: MIGRATION-2.0.md
+91Lines changed: 91 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -77,6 +77,97 @@ The `Tool` record now models `inputSchema` (and `outputSchema`) as arbitrary JSO
77
77
- Java code that used `Tool.inputSchema()` as a `JsonSchema` must switch to `Map<String, Object>` (or copy into your own schema wrapper).
78
78
-`Tool.Builder.inputSchema(JsonSchema)` remains as a **deprecated** helper that maps the old record into a map; prefer `inputSchema(Map)` or `inputSchema(McpJsonMapper, String)`.
79
79
80
+
### Required MCP spec fields are enforced at construction time
81
+
82
+
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.
**Action:** Audit any code that constructs these records with potentially-null values and provide valid, non-null arguments.
95
+
96
+
**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.
97
+
98
+
**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.
99
+
100
+
### `PromptReference` discriminator pinning and equality
101
+
102
+
`PromptReference` keeps its `(type, name, title)` record components, so positional construction from 1.x still compiles. Two behavioural changes:
103
+
104
+
- The compact constructor pins `type` to `ref/prompt`. Any non-null value other than `ref/prompt` is replaced with `ref/prompt` and a `WARN` is logged. The legacy two-arg `PromptReference(String type, String name)` constructor remains `@Deprecated` and routes through the canonical constructor, so it triggers the same WARN.
105
+
-`equals`/`hashCode` now consider `name` only (title and type are ignored). Two refs with the same name but different titles compare equal.
106
+
107
+
**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.
108
+
109
+
`CompleteReference.identifier()` is `@Deprecated` and now returns `null` via a default method on the interface.
110
+
111
+
### `ResourceReference` record component reduced
112
+
113
+
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.
114
+
115
+
### Builder API: required-first factories; old setters/no-arg builders deprecated
116
+
117
+
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.
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:
`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.
168
+
169
+
The 1.x canonical 4-arg constructors continue to compile.
170
+
80
171
### Optional JSON Schema validation on `tools/call` (server)
81
172
82
173
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.
Copy file name to clipboardExpand all lines: conformance-tests/client-jdk-http-client/src/main/java/io/modelcontextprotocol/conformance/client/ConformanceJdkClientMcpClient.java
Copy file name to clipboardExpand all lines: conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/DefaultScenario.java
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -69,7 +69,7 @@ public void execute(String serverUrl) {
Copy file name to clipboardExpand all lines: conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/PreRegistrationScenario.java
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -60,7 +60,7 @@ public void execute(String serverUrl) {
0 commit comments