Repository navigation
Description
Bug description
When a tool has an outputSchema and returns valid structuredContent with empty content, the SDK adds a text fallback but drops _meta when rebuilding the result.
Both McpAsyncServer and McpStatelessAsyncServer have this issue. Results with non-empty content keep their metadata.
Environment
- Java SDK:
2.2.0-SNAPSHOT, main atc531bb80 - Java: JDK 17.0.12
- Reproduced with the existing servlet integration tests
Steps to reproduce
-
Register a tool with an output schema:
{"type": "object"} -
Return structured content and metadata from the handler, without adding
text content:return CallToolResult.builder() .structuredContent(Map.of("value", 42)) .meta(Map.of("example.com/trace-id", "trace-123")) .build();
-
Call the tool from an MCP client and inspect the result.
Expected behavior
The generated text, structured content, and metadata are all present:
{
"content": [
{"type": "text", "text": "{\"value\":42}"}
],
"isError": false,
"structuredContent": {"value": 42},
"_meta": {"example.com/trace-id": "trace-123"}
}Actual behavior
The generated text and structuredContent are present, but _meta is
missing. The Java client receives result.meta() == null.
Minimal Complete Reproducible example
Failing JUnit 5 test using Mockito, in the mcp-test module,
package io.modelcontextprotocol.server:
@Test
void structuredOutputTextFallbackPreservesMetadata() {
var transport = mock(McpStatelessServerTransport.class);
Map<String, Object> metadata = Map.of("example.com/trace-id", "trace-123");
var tool = McpSchema.Tool.builder("example", Map.of("type", "object"))
.outputSchema(Map.of("type", "object"))
.build();
McpServer.sync(transport)
.serverInfo("test-server", "1.0.0")
.toolCall(tool, (context, request) -> McpSchema.CallToolResult.builder()
.structuredContent(Map.of("value", 42))
.meta(metadata)
.build())
.build();
var handlerCaptor = ArgumentCaptor.forClass(McpStatelessServerHandler.class);
verify(transport).setMcpHandler(handlerCaptor.capture());
var response = handlerCaptor.getValue()
.handleRequest(McpTransportContext.EMPTY,
new McpSchema.JSONRPCRequest(McpSchema.METHOD_TOOLS_CALL, "1",
McpSchema.CallToolRequest.builder("example").build()))
.block(Duration.ofSeconds(5));
// Fails before the fix: actual metadata is null.
assertThat(((McpSchema.CallToolResult) response.result()).meta()).isEqualTo(metadata);
}Proposed fix
In the Utils.isEmpty(result.content()) branches of both
McpAsyncServer and McpStatelessAsyncServer, preserve the metadata
when rebuilding the result:
return CallToolResult.builder()
.content(List.of(
McpSchema.TextContent.builder(validation.jsonStructuredOutput()).build()
))
.isError(result.isError())
.structuredContent(result.structuredContent())
.meta(result.meta())
.build();Tested locally with the existing SSE, Streamable HTTP, and stateless HTTP integration tests.
I’d like to submit a fix with regression tests, Please let me
know if this approach looks good and whether I can take this on.
Activity