Repository navigation
Fix MCP server 500 error on unknown client capability fields - #26008
Merged
Merged
Conversation
The global Graylog ObjectMapper (with SnakeCaseStrategy and custom modules) was being used to deserialize MCP SDK protocol types, which bypassed the SDK's @JsonIgnoreProperties(ignoreUnknown = true) annotations. This caused 500 errors when clients sent forward-compatible capability fields like capabilities.sampling.tools. Introduce a plain ObjectMapper qualified with @McpProtocolObjectMapper for deserializing McpSchema.* types, while keeping the global mapper for serializing Graylog domain objects in tool results. Fixes: #25956 Co-Authored-By: Claude Opus 4.6 <[EMAIL_ADDRESS_REDACTED]>
Drop the @McpProtocolObjectMapper annotation and Guice binding in favor of directly instantiating a plain ObjectMapper as a field in McpService and McpRestResource. Co-Authored-By: Claude Opus 4.6 <[EMAIL_ADDRESS_REDACTED]>
The MCP Java SDK has a known bug (modelcontextprotocol/java-sdk#766) where capability sub-records like ClientCapabilities.Sampling lack @JsonIgnoreProperties(ignoreUnknown = true). As a temporary workaround we explicitly disable FAIL_ON_UNKNOWN_PROPERTIES to tolerate forward-compatible fields from newer MCP clients. Co-Authored-By: Claude Opus 4.6 <[EMAIL_ADDRESS_REDACTED]>
…RTIES Re-introduce the @McpProtocolObjectMapper qualifier annotation and Guice binding (from the first commit of this PR) instead of inline new ObjectMapper() instances. This centralizes configuration in McpServerModule and makes the mapper injectable/testable. The binding now includes FAIL_ON_UNKNOWN_PROPERTIES=false to handle forward-compatible fields from newer MCP clients. Workaround for modelcontextprotocol/java-sdk#766 Remove once the SDK adds @JsonIgnoreProperties(ignoreUnknown=true) to its capability records. Co-Authored-By: Claude Opus 4.6 <[EMAIL_ADDRESS_REDACTED]>
Verifies that the protocol ObjectMapper tolerates forward-compatible fields like capabilities.sampling.tools that the SDK's empty Sampling record doesn't declare. Regression test for #25956 Co-Authored-By: Claude Opus 4.6 <[EMAIL_ADDRESS_REDACTED]>
janheise
reviewed
Jun 5, 2026
| * Workaround for <a href="https://github.com/modelcontextprotocol/java-sdk/issues/766">java-sdk#766</a>. | ||
| * Remove once the SDK adds @JsonIgnoreProperties(ignoreUnknown=true) to its capability records. | ||
| */ | ||
| @BindingAnnotation |
Contributor
There was a problem hiding this comment.
according to https://github.com/google/guice/wiki/BindingAnnotations, @BindingAnnotation is "old" and @Qualifier the current way to tag the interface
janheise
self-requested a review
June 5, 2026 12:04
janheise
approved these changes
Jun 5, 2026
janheise
left a comment
Contributor
There was a problem hiding this comment.
LGTM, just one small comment
monrax
added a commit
that referenced
this pull request
Jul 2, 2026
* Use MCP SDK's own JSON mapper for protocol (de)serialization mcp-sdk 2.0.0 adds @JsonIgnoreProperties(ignoreUnknown=true) to every McpSchema wire record (including the ClientCapabilities sub-records), which fixes java-sdk#766 -- the bug that forced the FAIL_ON_UNKNOWN_PROPERTIES=false workaround introduced in #26008. Drop that workaround and the bespoke @McpProtocolObjectMapper qualifier, and inject the SDK's own McpJsonMapper (JacksonMcpJsonMapperSupplier) instead. The distinct McpJsonMapper type disambiguates it from the global Graylog ObjectMapper -- whose SnakeCaseStrategy and custom modules must not touch protocol messages -- so no qualifier is needed. The four protocol convertValue sites target plain (non-generic) records, so the inferred TypeReference is replaced with the explicit target class. The bug #2 regression test (testInitializeToleratesUnknownCapabilityFields) now exercises the SDK mapper directly, proving the workaround is no longer required. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Migrate deprecated McpSchema constructors to builders mcp-sdk 2.0.0 deprecates the all-args constructors and no-arg builders of its McpSchema wire records in favour of required-first builder factories (builder(req1, req2, ...)). Migrate the deprecated construction sites: - McpService: Implementation, ListResourcesResult, TextResourceContents, ReadResourceResult, ResourceTemplate, ListResourceTemplatesResult, ListToolsResult, TextContent (x4), ListPromptsResult. - Stream/Dashboard/EventDefinition resource providers: Resource, in both the list() (deprecated 8-arg ctor) and read() (deprecated no-arg builder() + uri()/name() setters) paths. Behaviour is unchanged; the builders delegate to the same canonical constructors. Canonical (non-deprecated) constructors are left as-is: InitializeResult (5-arg), ServerCapabilities and its capability sub-records, and JSONRPCResponse/JSONRPCError. The Tool builder (Tool.builder() + inputSchema(JsonSchema)) is intentionally left for the follow-up inputSchema -> Map refactor, since its non-deprecated factory requires the input schema as a Map up front. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Model Tool inputSchema as Map instead of deprecated JsonSchema mcp-sdk 2.0.0 deprecates the McpSchema.JsonSchema record (java-sdk#749) in favour of modelling a tool's inputSchema as an arbitrary JSON Schema Map<String, Object>, matching how outputSchema is already handled. Change Tool's inputSchema field/accessor from McpSchema.JsonSchema to Map<String, Object>, generated the same way as outputSchema. Since the MCP spec requires every tool to declare an input schema (the Tool record's inputSchema is non-null), inputSchema() now returns a non-null Map, defaulting an empty generated schema to {"type":"object"}. This lets McpService use the non-deprecated Tool.builder(name, inputSchema) factory, clearing the last two MCP deprecation warnings in production code (Tool.builder() no-arg and inputSchema(JsonSchema)). JsonSchema is no longer referenced anywhere in our code. Wire change: the emitted inputSchema now includes any top-level JSON Schema keywords that the old JsonSchema record dropped -- in particular the "$schema":"https://json-schema.org/draft/2020-12/schema" dialect declaration our victools generator produces. Previously convertValue(node, JsonSchema.class) narrowed the document to {type, properties, required, additionalProperties, $defs, definitions} and the deprecated builder.inputSchema(JsonSchema) helper rebuilt the map from only those fields, silently stripping $schema. The map path preserves it, so input schemas now advertise their dialect just like output schemas already did (outputSchema has always gone through convertValue(node, Map)). The change is additive and spec-aligned: SEP-1613 treats a missing $schema as 2020-12, which is exactly the dialect now declared. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Validate tool-call arguments against the tool input schema mcp-sdk 2.0.0 added server-side validation of tool arguments against the declared inputSchema (java-sdk#873), but that lives in the SDK server runtime, which we don't use -- we hand-roll JSON-RPC dispatch in McpService. Add the equivalent ourselves so a tools/call with arguments that don't match the tool's input schema is rejected with a CallToolResult(isError=true) carrying the validation message, matching the SDK's behaviour, instead of being silently coerced by Jackson. Validation uses the SDK's own (non-deprecated) JsonSchemaValidator (io.modelcontextprotocol.json.schema; the DefaultJsonSchemaValidator caches compiled schemas), so arguments are checked against the same 2020-12 dialect our schemas now declare. The tool's advertised inputSchema (Map) is the schema used for validation. Gated behind a new McpConfiguration.enableInputValidation flag, defaulting to false to mirror the existing enable_output_schema opt-in (and so existing stored configs, which lack the field, keep current behaviour). When disabled, McpService behaves exactly as before. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Pass McpConfiguration into McpService.handle instead of re-reading it McpService.handle re-fetched McpConfiguration from ClusterConfigService on every tools/call (to read enableInputValidation), which is an uncached MongoDB lookup -- and McpRestResource had already read the same document earlier in the same request to check enableRemoteAccess. That was a redundant per-request DB round-trip. Thread the already-read McpConfiguration from McpRestResource into handle() and use it for the validation gate, so the config is read once per request. McpService no longer depends on ClusterConfigService. The no-arg-config handle overload used by tests defaults to McpConfiguration.DEFAULT_VALUES (validation off), preserving existing behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Parse JSON-RPC envelope in McpService; drop the mapper from McpRestResource McpRestResource held its own McpJsonMapper purely to turn the request body into a JSONRPCRequest, duplicating the mapper McpService already owns. It also hand-rolled envelope inspection (containsAnyRequests) and then blindly convertValue'd the payload into a JSONRPCRequest -- which would coerce a notification or response into a half-null request. Add McpService.parseMessage(JsonNode), which uses the SDK's McpSchema.deserializeJsonRpcMessage to parse the body into a typed JSONRPCMessage (request / notification / response). McpRestResource now delegates parsing to it and dispatches on the type: requests are handled, notifications and responses get 202 Accepted. This makes McpService the sole owner of the protocol mapper, so McpRestResource no longer depends on it. Parsing runs in its own try/catch: an unrecognisable payload returns 400 Bad Request instead of being silently accepted (as the old containsAnyRequests path did). This also keeps the request-handling path free of null ids -- a parsed JSONRPCRequest always carries one -- so we never try to build an error JSONRPCResponse with a null id, which 2.0.0's constructor rejects. HTTP behaviour is otherwise preserved (403 remote-access, 202 notification/response, 405 SSE, 200 result, 400 McpError, 500 otherwise). Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Fall back to the entity id for blank-titled resources on read The resource providers' read() built McpSchema.Resource with the entity title as the resource name, with no fallback. Since the REST API allows a blank title, a blank-titled stream/dashboard/event definition made the SDK 2.0.0 Resource constructor throw "name must not be empty", which McpService maps to RESOURCE_NOT_FOUND -- so a readable entity looked missing. Apply the same Strings.isNullOrEmpty(title) ? id : title fallback that list() already uses, so read() and list() agree and blank-titled entities remain readable. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Drop the now-unused throws IOException from McpRestResource.post Since envelope parsing moved into its own try/catch, the only checked IOException (from McpService.parseMessage) is caught locally and never propagates out of post(). Remove the dead throws clause and its import. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Add resource provider read() tests The Stream/Dashboard/EventDefinition resource providers had no unit tests, so the read() name-fallback fix (blank title -> entity id) was only covered live. Add focused tests per provider for read(): blank title falls back to the id, a present title is used as-is, and missing/forbidden entities return empty. ViewDTO and EventDefinitionDto are @autovalue; they're mocked here (the DoNotMockAutoValue lint is a warning the codebase already tolerates) since we only need their id/title/description accessors. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Inline the protocol mapper and schema validator in McpService Both the McpJsonMapper and the JsonSchemaValidator are now used only by McpService (McpRestResource stopped using the mapper when envelope parsing moved into McpService). They are stateless, dependency-free SDK helpers constructed from a no-arg supplier, so binding them in Guice and injecting them was DI ceremony with no payoff -- no shared instance, no alternative implementation, and the test seam went unused (the validation tests use the real validator). Construct them as private final fields in McpService and drop the two McpServerModule bindings and the two constructor parameters. Behaviour is unchanged; McpServiceTest no longer has to supply them. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Add UI switch for enable_input_validation option * Drop the test-only 3-arg McpService.handle overload The 3-arg handle(permissionHelper, request, sessionId) overload existed only as test convenience -- the sole production caller (McpRestResource) uses the full handle(..., protocolVersion, config) signature. Remove it so the production API is the single real method, and move the convenience into McpServiceTest as a private handle(request) helper that supplies the test defaults. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]>
monrax
added a commit
that referenced
this pull request
Jul 7, 2026
* Fit the MCP server to mcp-sdk 2.0.0 (#26526) * Use MCP SDK's own JSON mapper for protocol (de)serialization mcp-sdk 2.0.0 adds @JsonIgnoreProperties(ignoreUnknown=true) to every McpSchema wire record (including the ClientCapabilities sub-records), which fixes java-sdk#766 -- the bug that forced the FAIL_ON_UNKNOWN_PROPERTIES=false workaround introduced in #26008. Drop that workaround and the bespoke @McpProtocolObjectMapper qualifier, and inject the SDK's own McpJsonMapper (JacksonMcpJsonMapperSupplier) instead. The distinct McpJsonMapper type disambiguates it from the global Graylog ObjectMapper -- whose SnakeCaseStrategy and custom modules must not touch protocol messages -- so no qualifier is needed. The four protocol convertValue sites target plain (non-generic) records, so the inferred TypeReference is replaced with the explicit target class. The bug #2 regression test (testInitializeToleratesUnknownCapabilityFields) now exercises the SDK mapper directly, proving the workaround is no longer required. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Migrate deprecated McpSchema constructors to builders mcp-sdk 2.0.0 deprecates the all-args constructors and no-arg builders of its McpSchema wire records in favour of required-first builder factories (builder(req1, req2, ...)). Migrate the deprecated construction sites: - McpService: Implementation, ListResourcesResult, TextResourceContents, ReadResourceResult, ResourceTemplate, ListResourceTemplatesResult, ListToolsResult, TextContent (x4), ListPromptsResult. - Stream/Dashboard/EventDefinition resource providers: Resource, in both the list() (deprecated 8-arg ctor) and read() (deprecated no-arg builder() + uri()/name() setters) paths. Behaviour is unchanged; the builders delegate to the same canonical constructors. Canonical (non-deprecated) constructors are left as-is: InitializeResult (5-arg), ServerCapabilities and its capability sub-records, and JSONRPCResponse/JSONRPCError. The Tool builder (Tool.builder() + inputSchema(JsonSchema)) is intentionally left for the follow-up inputSchema -> Map refactor, since its non-deprecated factory requires the input schema as a Map up front. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Model Tool inputSchema as Map instead of deprecated JsonSchema mcp-sdk 2.0.0 deprecates the McpSchema.JsonSchema record (java-sdk#749) in favour of modelling a tool's inputSchema as an arbitrary JSON Schema Map<String, Object>, matching how outputSchema is already handled. Change Tool's inputSchema field/accessor from McpSchema.JsonSchema to Map<String, Object>, generated the same way as outputSchema. Since the MCP spec requires every tool to declare an input schema (the Tool record's inputSchema is non-null), inputSchema() now returns a non-null Map, defaulting an empty generated schema to {"type":"object"}. This lets McpService use the non-deprecated Tool.builder(name, inputSchema) factory, clearing the last two MCP deprecation warnings in production code (Tool.builder() no-arg and inputSchema(JsonSchema)). JsonSchema is no longer referenced anywhere in our code. Wire change: the emitted inputSchema now includes any top-level JSON Schema keywords that the old JsonSchema record dropped -- in particular the "$schema":"https://json-schema.org/draft/2020-12/schema" dialect declaration our victools generator produces. Previously convertValue(node, JsonSchema.class) narrowed the document to {type, properties, required, additionalProperties, $defs, definitions} and the deprecated builder.inputSchema(JsonSchema) helper rebuilt the map from only those fields, silently stripping $schema. The map path preserves it, so input schemas now advertise their dialect just like output schemas already did (outputSchema has always gone through convertValue(node, Map)). The change is additive and spec-aligned: SEP-1613 treats a missing $schema as 2020-12, which is exactly the dialect now declared. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Validate tool-call arguments against the tool input schema mcp-sdk 2.0.0 added server-side validation of tool arguments against the declared inputSchema (java-sdk#873), but that lives in the SDK server runtime, which we don't use -- we hand-roll JSON-RPC dispatch in McpService. Add the equivalent ourselves so a tools/call with arguments that don't match the tool's input schema is rejected with a CallToolResult(isError=true) carrying the validation message, matching the SDK's behaviour, instead of being silently coerced by Jackson. Validation uses the SDK's own (non-deprecated) JsonSchemaValidator (io.modelcontextprotocol.json.schema; the DefaultJsonSchemaValidator caches compiled schemas), so arguments are checked against the same 2020-12 dialect our schemas now declare. The tool's advertised inputSchema (Map) is the schema used for validation. Gated behind a new McpConfiguration.enableInputValidation flag, defaulting to false to mirror the existing enable_output_schema opt-in (and so existing stored configs, which lack the field, keep current behaviour). When disabled, McpService behaves exactly as before. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Pass McpConfiguration into McpService.handle instead of re-reading it McpService.handle re-fetched McpConfiguration from ClusterConfigService on every tools/call (to read enableInputValidation), which is an uncached MongoDB lookup -- and McpRestResource had already read the same document earlier in the same request to check enableRemoteAccess. That was a redundant per-request DB round-trip. Thread the already-read McpConfiguration from McpRestResource into handle() and use it for the validation gate, so the config is read once per request. McpService no longer depends on ClusterConfigService. The no-arg-config handle overload used by tests defaults to McpConfiguration.DEFAULT_VALUES (validation off), preserving existing behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Parse JSON-RPC envelope in McpService; drop the mapper from McpRestResource McpRestResource held its own McpJsonMapper purely to turn the request body into a JSONRPCRequest, duplicating the mapper McpService already owns. It also hand-rolled envelope inspection (containsAnyRequests) and then blindly convertValue'd the payload into a JSONRPCRequest -- which would coerce a notification or response into a half-null request. Add McpService.parseMessage(JsonNode), which uses the SDK's McpSchema.deserializeJsonRpcMessage to parse the body into a typed JSONRPCMessage (request / notification / response). McpRestResource now delegates parsing to it and dispatches on the type: requests are handled, notifications and responses get 202 Accepted. This makes McpService the sole owner of the protocol mapper, so McpRestResource no longer depends on it. Parsing runs in its own try/catch: an unrecognisable payload returns 400 Bad Request instead of being silently accepted (as the old containsAnyRequests path did). This also keeps the request-handling path free of null ids -- a parsed JSONRPCRequest always carries one -- so we never try to build an error JSONRPCResponse with a null id, which 2.0.0's constructor rejects. HTTP behaviour is otherwise preserved (403 remote-access, 202 notification/response, 405 SSE, 200 result, 400 McpError, 500 otherwise). Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Fall back to the entity id for blank-titled resources on read The resource providers' read() built McpSchema.Resource with the entity title as the resource name, with no fallback. Since the REST API allows a blank title, a blank-titled stream/dashboard/event definition made the SDK 2.0.0 Resource constructor throw "name must not be empty", which McpService maps to RESOURCE_NOT_FOUND -- so a readable entity looked missing. Apply the same Strings.isNullOrEmpty(title) ? id : title fallback that list() already uses, so read() and list() agree and blank-titled entities remain readable. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Drop the now-unused throws IOException from McpRestResource.post Since envelope parsing moved into its own try/catch, the only checked IOException (from McpService.parseMessage) is caught locally and never propagates out of post(). Remove the dead throws clause and its import. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Add resource provider read() tests The Stream/Dashboard/EventDefinition resource providers had no unit tests, so the read() name-fallback fix (blank title -> entity id) was only covered live. Add focused tests per provider for read(): blank title falls back to the id, a present title is used as-is, and missing/forbidden entities return empty. ViewDTO and EventDefinitionDto are @autovalue; they're mocked here (the DoNotMockAutoValue lint is a warning the codebase already tolerates) since we only need their id/title/description accessors. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Inline the protocol mapper and schema validator in McpService Both the McpJsonMapper and the JsonSchemaValidator are now used only by McpService (McpRestResource stopped using the mapper when envelope parsing moved into McpService). They are stateless, dependency-free SDK helpers constructed from a no-arg supplier, so binding them in Guice and injecting them was DI ceremony with no payoff -- no shared instance, no alternative implementation, and the test seam went unused (the validation tests use the real validator). Construct them as private final fields in McpService and drop the two McpServerModule bindings and the two constructor parameters. Behaviour is unchanged; McpServiceTest no longer has to supply them. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> * Add UI switch for enable_input_validation option * Drop the test-only 3-arg McpService.handle overload The 3-arg handle(permissionHelper, request, sessionId) overload existed only as test convenience -- the sole production caller (McpRestResource) uses the full handle(..., protocolVersion, config) signature. Remove it so the production API is the single real method, and move the convenience into McpServiceTest as a private handle(request) helper that supplies the test defaults. Co-Authored-By: Claude Opus 4.8 (1M context) <[EMAIL_ADDRESS_REDACTED]> (cherry picked from commit 1757bc5)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Introduce a dedicated protocol-level
ObjectMapperinMcpServiceandMcpRestResourcefor deserializing MCP SDK types (InitializeRequest,ReadResourceRequest,CallToolRequest,GetPromptRequest). This mapper is configured withFAIL_ON_UNKNOWN_PROPERTIES = falseto tolerate forward-compatible fields from newer MCP clients.The global Graylog
ObjectMapperis retained inMcpServiceonly for serializing tool results (domain types likeTabularResponse).Motivation and Context
The MCP Java SDK has a known bug (modelcontextprotocol/java-sdk#766) where capability sub-records like
ClientCapabilities.Samplinglack@JsonIgnoreProperties(ignoreUnknown = true). Combined with the global GraylogObjectMapperbeing used for protocol deserialization (which addsSnakeCaseStrategyand other custom modules), this caused 500 Internal Server Errors when clients sent forward-compatible fields likecapabilities.sampling.tools.Fixes #25956
How Has This Been Tested?
McpServiceTest(15 tests) andMcpRestResourceTest(2 tests) pass.{"capabilities":{"sampling":{"tools":{}}}}no longer causes a 500.Screenshots (if appropriate):
N/A
Types of changes
Checklist: