Skip to content

Tool result _meta is lost when the server automatically adds text content for structured output #1330

Open

Description

@chen-nay

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 at c531bb80
  • Java: JDK 17.0.12
  • Reproduced with the existing servlet integration tests

Steps to reproduce

  1. Register a tool with an output schema:

    {"type": "object"}
  2. 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();
  3. 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

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions