spring-projects / spring-projects/spring-ai
MCP generateOutputSchema produces an unsatisfiable schema for optional/nullable fields (non-nullable type + required)
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 9.5k
- Forks
- 2.9k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 6
Description
Summary
@McpTool(generateOutputSchema = true) generates an output schema that no
response can satisfy when the return type has an optional (nullable) member.
Every property is typed non-nullable and listed in required, so a null value
fails the type check and omitting the field fails the required check. The MCP SDK
then rejects a correct response at call time.
This is not a nullability-annotation or serialization problem: there is no
serialization of an optional field that validates against the generated schema.
Environment
- Spring AI 2.0.1 (
spring-ai-starter-mcp-server-webmvc) - MCP Java SDK 2.0.0
- Spring Boot 4.1.1, Java 21
- Transport: Streamable HTTP,
SYNC
Minimal reproduction
One tool, one record, one optional field:
@Component
public class OptionalFieldTool {
// endedAt is null whenever the item has not ended — an ordinary shape for
// anything with a validity period, a completion time or an upper bound.
public record Item(String id, String endedAt) { }
@McpTool(name = "fixture.get_item",
description = "Returns an item whose endedAt is null when it has not ended.",
generateOutputSchema = true)
public Item getItem(@McpToolParam(description = "Anything.", required = true) String id) {
return new Item(id, null);
}
}
Generated outputSchema from tools/list
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"endedAt": { "type": "string" },
"id": { "type": "string" }
},
"required": [ "endedAt", "id" ]
}
endedAt is typed string (not ["string","null"]) and is in required.
Actual behaviour
tools/call on fixture.get_item returns isError: true:
Tool (fixture.get_item) output validation failed: Validation failed:
JSON schema validation errors: [/endedAt: null found, string expected]
Expected behaviour
A nullable/optional component should be generated as optional — omitted from
required, and/or typed to admit null (e.g. "type": ["string", "null"]) — so
that a valid response validates.
Why the obvious workarounds do not help
- Annotating the field
@org.springframework.lang.Nullable— no effect on
the generated schema. - Omitting nulls from serialization (including via the SDK's
McpJsonMapperSupplierServiceLoader hook) — moves the failure from the type
check to therequiredcheck, because the field is marked required as well.
Because both halves are present, an optional member has no valid representation
at all.
Impact
Any tool whose response type has an optional member cannot use
generateOutputSchema. In our case enabling it across a 16-tool server broke the
primary read tool on the first call containing a null; it fails at neither build
time nor startup, only on the first real response. We have disabled the flag and
are tracking it as interoperability debt.
Reproduction project
The reproduction above is self-contained: a Boot 4.1.1 app with the one component
shown, spring.ai.mcp.server.protocol=STREAMABLE, type=SYNC, and a test that
performs initialize → tools/list → tools/call. Happy to attach or link a
repo if that is useful.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the @McpTool(generateOutputSchema = true) output-schema generation path and run the self-contained reproduction through initialize, tools/list, and tools/call. Verify that a nullable endedAt value can satisfy the generated schema without failing either type validation or the required-field check.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring-boot
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 64/100