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
Allow strict validation of CallToolResult.content during deserialization #1148
Using mcp-core:2.0.1 on Java 17, we noticed that a tools/call response without content is accepted and converted to a result with an empty content list.
Initialization and tool discovery complete normally. The negotiated protocol version is 2025-11-25.
Example
The server returns this response, with an ID matching the tool request:
In our integration, the first response was recorded as a successful tool call with empty output and passed to the model. The application could no longer tell that a required field had been missing.
Request
I understand that this is intentional for compatibility, as described in the migration guide. Could the SDK provide an opt-in strict mode that validates required fields before substituting defaults?
For CallToolResult.content, that mode would reject a missing field or explicit null, while still accepting an explicit empty array. Valid text and structured results would continue to work.
Rejecting empty lists in the application isn't a solution, because content: [] is valid. We need to distinguish it from a missing field before that information is lost.
Is there already a supported way to enforce this validation? If not, a strict-mode option or a validation hook would help.
Good catch. The 2025-11-25 schema marks content required, so the lenient default is a wire-level compatibility choice, and the information loss happens at deserialization, where missing and explicit null both collapse into the same empty default. The clean way to keep that distinction is presence detection, not null checks: in Jackson, deserialize to the tree and check node.has("content") before mapping to the record. has() returns false for both absent and explicit null in most configurations, so you need isNull() on the same node if strict mode wants to treat explicit null differently from absence.
For the opt-in strict mode, a deserialization-level toggle on McpSchema.CallToolResult (or a builder flag on the client/session factory) is the right shape, defaulting to lenient so 2.0 migration behavior is unchanged. Two implementation notes:
Throw a protocol-level error, not a plain runtime exception, when strict mode rejects: ProtocolException with JSON-RPC code -32602 (invalid params) is what an MCP client should surface for a malformed tool result, and it lets applications distinguish "server returned garbage" from "tool legitimately returned no content".
Keep the strict gate at the deserialization boundary only. Substituting List.of() hides the difference from the model as well, but the SDK cannot decide policy for the application; an opt-in flag at the boundary is the least intrusive contract.
If the Python and TypeScript SDKs already handle missing content differently (one raises, one defaults), aligning the opt-in strict behavior across SDKs would be worth a note in the migration guide. We run servers that straddle all three SDKs at corpusiq.io, and that divergence is where interop bugs hide.
Activity
Ben-Home commentedon Sep 30, 2026
Good catch. The 2025-11-25 schema marks
contentrequired, so the lenient default is a wire-level compatibility choice, and the information loss happens at deserialization, where missing and explicit null both collapse into the same empty default. The clean way to keep that distinction is presence detection, not null checks: in Jackson, deserialize to the tree and checknode.has("content")before mapping to the record.has()returns false for both absent and explicit null in most configurations, so you needisNull()on the same node if strict mode wants to treat explicit null differently from absence.For the opt-in strict mode, a deserialization-level toggle on
McpSchema.CallToolResult(or a builder flag on the client/session factory) is the right shape, defaulting to lenient so 2.0 migration behavior is unchanged. Two implementation notes:ProtocolExceptionwith JSON-RPC code -32602 (invalid params) is what an MCP client should surface for a malformed tool result, and it lets applications distinguish "server returned garbage" from "tool legitimately returned no content".List.of()hides the difference from the model as well, but the SDK cannot decide policy for the application; an opt-in flag at the boundary is the least intrusive contract.If the Python and TypeScript SDKs already handle missing
contentdifferently (one raises, one defaults), aligning the opt-in strict behavior across SDKs would be worth a note in the migration guide. We run servers that straddle all three SDKs at corpusiq.io, and that divergence is where interop bugs hide.