Skip to content

Allow strict validation of CallToolResult.content during deserialization #1148

Open

Description

@kele5555

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:

{
  "jsonrpc": "2.0",
  "id": "request-2",
  "result": {
    "isError": false
  }
}

CallToolResult.fromJson logs a warning and replaces the missing content with List.of().

The application then receives the same result it would get from this valid response:

{
  "jsonrpc": "2.0",
  "id": "request-2",
  "result": {
    "content": [],
    "isError": false
  }
}

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.

References:

Activity

  1. Ben-Home commented on Sep 30, 2026

    @Ben-Home

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Nice to haves, rare edge casesarea/client

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions