Skip to content

feat!: guard required MCP spec fields against null#928

Draft
chemicL wants to merge 12 commits intojson-compatibilityfrom
json-schema-required-fields
Draft

feat!: guard required MCP spec fields against null#928
chemicL wants to merge 12 commits intojson-compatibilityfrom
json-schema-required-fields

Conversation

@chemicL
Copy link
Copy Markdown
Member

@chemicL chemicL commented Apr 20, 2026

Compact canonical constructors with Assert.notNull are added to seven records:

  • JSONRPCError (code, message),
  • ProgressNotification (progressToken, progress),
  • LoggingMessageNotification (level, data),
  • CreateMessageRequest (messages, maxTokens),
  • CallToolResult (content),
  • SamplingMessage (role, content),
  • ElicitRequest (message, requestedSchema).

This PR is stacked on top of #927

Motivation and Context

Records in McpSchema whose fields the MCP spec marks as required were storing them as nullable Java types with no validation. A caller constructing one of these records with null for a required field would silently produce invalid wire JSON because @JsonInclude(NON_ABSENT) omits null values without complaint.

At the same time, we try to be lenient to not reject existing integrations that fail to comply with the specification and don't provide required fields. In this case we replace missing values with defaults and log the fact.

How Has This Been Tested?

Test code that constructed these records without required fields was updated to supply valid values; those tests were testing capability-check or error-path logic that fires after construction, so the missing fields were incidental rather than intentional.

Breaking Changes

Behavioural - validation failures for required fields that turn out to be null or missing.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Records in McpSchema whose fields the MCP spec marks as required were
storing them as nullable Java types with no validation. A caller
constructing one of these records with null for a required field would
silently produce invalid wire JSON because @JsonInclude(NON_ABSENT) omits
null values without complaint.

Compact canonical constructors with Assert.notNull are added to seven
records:
- JSONRPCError (code, message),
- ProgressNotification (progressToken, progress),
- LoggingMessageNotification (level, data),
- CreateMessageRequest (messages, maxTokens),
- CallToolResult (content),
- SamplingMessage (role, content),
- ElicitRequest (message, requestedSchema).

Test code that constructed these records without required fields was
updated to supply valid values; those tests were testing capability-check
or error-path logic that fires after construction, so the missing fields
were incidental rather than intentional.

Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
@chemicL chemicL marked this pull request as draft April 20, 2026 17:32
chemicL and others added 10 commits April 21, 2026 00:21
Deprecate no-arg builder() factories and Builder() constructors on
CreateMessageRequest, ElicitRequest, and LoggingMessageNotification.
Add new builder(required...) factories that validate required fields
upfront. Add new builders for ProgressNotification and JSONRPCError.
Null checks in private constructors and required-field setters ensure
invalid state cannot be introduced at any point in the builder chain.

Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Some clients or servers fail to provide required JSON fields so instead of
failing we replace them by default values and log with WARN level and allow the
application to make progress.

Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
…n (#972)

The MCP specification evolves continuously; domain types must absorb
new fields and subtypes without breaking existing clients or servers.
On the 1.x line this is structurally prevented by sealed interfaces,
which make it impossible to add a permitted subtype without breaking
exhaustive pattern-match switch expressions in caller code. This
commit opens the 2.0 release line, where those constraints are lifted
and serialization is made self-contained — independent of any global
ObjectMapper configuration.

Breaking changes for users migrating from 1.x

- Sealed interfaces removed from JSONRPCMessage, Request, Result,
  Notification, ResourceContents, CompleteReference and Content.
  Exhaustive switch expressions over these types must add a default
  branch.
- Prompt(name, description, null) no longer silently coerces null
  arguments to an empty list. Use Prompt.withDefaults() to preserve
  the previous behaviour.
- CompleteCompletion.total and .hasMore are now absent from the wire
  when not set, rather than being emitted as null.
- ServerParameters no longer carries Jackson annotations; it is an
  internal configuration class, not a wire type.

What now works that did not before

- CompleteReference polymorphic dispatch (PromptReference vs
  ResourceReference) works through a plain readValue or convertValue
  call — no hand-rolled map inspection required.
- LoggingLevel deserialization is lenient: unknown level strings
  produce null instead of throwing.
- All domain records now tolerate unknown JSON fields, so a client
  built against an older SDK version will not fail when a newer server
  sends fields it does not yet recognise.
- Null optional fields are consistently absent from serialized output
  regardless of ObjectMapper configuration.

Documentation

- CONTRIBUTING adds an "Evolving wire-serialized records" section: a
  9-rule recipe and example for adding a field safely.
- MIGRATION-2.0 documents all breaking changes listed above.

Follow-up coming next

Several spec-required fields (e.g. JSONRPCError.code/message,
ProgressNotification.progress, CreateMessageRequest.maxTokens,
CallToolResult.content) are stored as nullable Java types without a
null guard. If constructed with null, the NON_ABSENT rule silently
omits them, producing invalid wire JSON without throwing. Fix: compact
canonical constructors with Assert.notNull, following the pattern
already in JSONRPCRequest.

Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
…ts (#934)

Recent changes don't coerce null completion arguments to empty lists so we have
to check for null when handling prompt completions.

Resolves #932

Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant