Skip to content

Fix MCP tool output schemas to match actual serialized output#26556

Open
monrax wants to merge 5 commits into
masterfrom
fix/mcp-output-schema
Open

Fix MCP tool output schemas to match actual serialized output#26556
monrax wants to merge 5 commits into
masterfrom
fix/mcp-output-schema

Conversation

@monrax

@monrax monrax commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description

The MCP outputSchema generated for tool output types did not match what the server actually serializes, so MCP clients that validate structuredContent against the advertised schema (the official MCP SDKs do) rejected every search_messages, aggregate_messages, and list_fields response. Three mismatches, all fixed at the schema-generator level (SchemaGeneratorProvider / victools config):

  • Joda DateTime{"type": "object"}: effective_timerange.from/.to serialize as ISO-8601 strings. A new DateTimeAsStringModule maps date/time types (Joda ReadableInstant, java.util.Date, java.time instant types, i.e. everything the object mapper writes as ISO-8601 strings) to
    {"type": "string", "format": "date-time"}
  • java.lang.Object{"type": "object", "properties": {}}: the cells of TabularResponse#datarows (List<List<Object>>) hold strings, numbers, or null. EmptyObjectAsObjectModule now skips java.lang.Object so it stays an unconstrained {} schema.
  • @Nullable fields declared non-nullable: e.g. MappedFieldTypeDTO#unit serializes as null for most fields. A nullable check for javax/jakarta @Nullable now produces e.g. {"type": ["object", "null"]}.

A new round-trip test (ToolOutputSchemaComplianceTest) generates schemas and structuredContent exactly the way Tool/McpService do and validates one against the other using the MCP SDK's own JsonSchemaValidator so this class of mismatch can't silently regress.

Motivation and Context

Fixes #23980
Fixes #25314
Fixes #26402

All three issues are the same underlying bug reported against different MCP clients (Strands agents, Claude Desktop, Claude Code). With enable_output_schema turned on, the core search tools were unusable from any schema-validating client. The schema changes are strictly relaxing: every previously-valid payload remains valid, so nothing breaks for existing clients.

How Has This Been Tested?

  • New ToolOutputSchemaComplianceTest (round-trip schema-vs-serialization validation for TabularResponse and ListFieldsTool.Result, plus targeted assertions for the DateTime and Object schema shapes).
  • Full org.graylog.mcp.* test suite passes (McpServiceTest, McpRestResourceTest, MarkdownBuilderTest, resource provider tests).
  • Live-verified against a local devserver with enable_output_schema: true: before the fix, exhaustive JSON-Schema validation of structuredContent against the advertised outputSchema produced errors on every search_messages/aggregate_messages/list_fields call; after the fix, zero validation errors across all four schema-advertising tools (including get_system_status, confirming no regression).

Screenshots (if appropriate):

N/A

Types of changes

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

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have requested a documentation update.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.

🤖 Generated with Claude Code

The generated outputSchema for MCP tools did not match what the server
actually serializes, so schema-validating MCP clients rejected every
search_messages, aggregate_messages, and list_fields response:

- Joda DateTime was described as a bare object instead of an ISO-8601
  string (effective_timerange.from/to)
- java.lang.Object (TabularResponse datarows cells) was stamped
  "type": "object" by EmptyObjectAsObjectModule instead of remaining
  unconstrained
- @nullable fields (e.g. MappedFieldTypeDTO#unit) were not declared
  nullable although they serialize as null

Add a DateTimeAsStringModule mapping date/time types to
{"type": "string", "format": "date-time"}, skip java.lang.Object in
EmptyObjectAsObjectModule, and honor javax/jakarta @nullable via a
nullable check in SchemaGeneratorProvider. A new round-trip test
validates serialized tool output against the generated schemas using
the MCP SDK's own JsonSchemaValidator.

Co-Authored-By: Claude Fable 5 <[EMAIL_ADDRESS_REDACTED]>
monrax added 4 commits July 2, 2026 17:42
Validate structuredContent payloads captured verbatim from a live
server against the generated schemas. Unlike the round-trip tests,
which prove generator and serializer are consistent with each other,
these pin the wire format itself and fail if both sides ever drift
together.

Co-Authored-By: Claude Fable 5 <[EMAIL_ADDRESS_REDACTED]>
Validate an ISO-8601 date-time JSON value against the generated schema
for a metadata timerange, and assert that epoch millis and objects (the
shape the schema wrongly declared before the fix) are rejected.

Co-Authored-By: Claude Fable 5 <[EMAIL_ADDRESS_REDACTED]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment