diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 2044d8b38..537111651 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -101,6 +101,8 @@ public class McpAsyncServer { private final boolean validateToolInputs; + private final boolean duplicateStructuredContent; + private final McpSchema.ServerCapabilities serverCapabilities; private final McpSchema.Implementation serverInfo; @@ -133,13 +135,14 @@ public class McpAsyncServer { McpAsyncServer(McpServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper, McpServerFeatures.Async features, Duration requestTimeout, McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator, - boolean validateToolInputs) { + boolean validateToolInputs, boolean duplicateStructuredContent) { this.mcpTransportProvider = mcpTransportProvider; this.jsonMapper = jsonMapper; this.serverInfo = features.serverInfo(); this.serverCapabilities = features.serverCapabilities().mutate().logging().build(); this.instructions = features.instructions(); - this.tools.addAll(withStructuredOutputHandling(jsonSchemaValidator, features.tools())); + this.tools + .addAll(withStructuredOutputHandling(jsonSchemaValidator, duplicateStructuredContent, features.tools())); this.resources.putAll(features.resources()); this.resourceTemplates.putAll(features.resourceTemplates()); this.prompts.putAll(features.prompts()); @@ -147,6 +150,7 @@ public class McpAsyncServer { this.uriTemplateManagerFactory = uriTemplateManagerFactory; this.jsonSchemaValidator = jsonSchemaValidator; this.validateToolInputs = validateToolInputs; + this.duplicateStructuredContent = duplicateStructuredContent; Map> requestHandlers = prepareRequestHandlers(); Map notificationHandlers = prepareNotificationHandlers(features); @@ -164,13 +168,14 @@ public class McpAsyncServer { McpAsyncServer(McpStreamableServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper, McpServerFeatures.Async features, Duration requestTimeout, McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator, - boolean validateToolInputs) { + boolean validateToolInputs, boolean duplicateStructuredContent) { this.mcpTransportProvider = mcpTransportProvider; this.jsonMapper = jsonMapper; this.serverInfo = features.serverInfo(); this.serverCapabilities = features.serverCapabilities().mutate().logging().build(); this.instructions = features.instructions(); - this.tools.addAll(withStructuredOutputHandling(jsonSchemaValidator, features.tools())); + this.tools + .addAll(withStructuredOutputHandling(jsonSchemaValidator, duplicateStructuredContent, features.tools())); this.resources.putAll(features.resources()); this.resourceTemplates.putAll(features.resourceTemplates()); this.prompts.putAll(features.prompts()); @@ -178,6 +183,7 @@ public class McpAsyncServer { this.uriTemplateManagerFactory = uriTemplateManagerFactory; this.jsonSchemaValidator = jsonSchemaValidator; this.validateToolInputs = validateToolInputs; + this.duplicateStructuredContent = duplicateStructuredContent; Map> requestHandlers = prepareRequestHandlers(); Map notificationHandlers = prepareNotificationHandlers(features); @@ -357,7 +363,8 @@ public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica return Mono.error(e); } - var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification); + var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, + this.duplicateStructuredContent, toolSpecification); return Mono.defer(() -> { // Remove tools with duplicate tool names first @@ -384,8 +391,10 @@ private static class StructuredOutputCallToolHandler private final Map outputSchema; + private final boolean duplicateStructuredContent; + public StructuredOutputCallToolHandler(JsonSchemaValidator jsonSchemaValidator, - Map outputSchema, + Map outputSchema, boolean duplicateStructuredContent, BiFunction> delegateHandler) { Assert.notNull(jsonSchemaValidator, "JsonSchemaValidator must not be null"); @@ -393,6 +402,7 @@ public StructuredOutputCallToolHandler(JsonSchemaValidator jsonSchemaValidator, this.delegateCallToolResult = delegateHandler; this.outputSchema = outputSchema; + this.duplicateStructuredContent = duplicateStructuredContent; this.jsonSchemaValidator = jsonSchemaValidator; } @@ -440,7 +450,7 @@ public Mono apply(McpAsyncServerExchange exchange, McpSchema.Cal .build(); } - if (Utils.isEmpty(result.content())) { + if (this.duplicateStructuredContent && Utils.isEmpty(result.content())) { // For backwards compatibility, a tool that returns structured // content SHOULD also return functionally equivalent unstructured // content. (For example, serialized JSON can be returned in a @@ -461,17 +471,21 @@ public Mono apply(McpAsyncServerExchange exchange, McpSchema.Cal } private static List withStructuredOutputHandling( - JsonSchemaValidator jsonSchemaValidator, List tools) { + JsonSchemaValidator jsonSchemaValidator, boolean duplicateStructuredContent, + List tools) { if (Utils.isEmpty(tools)) { return tools; } - return tools.stream().map(tool -> withStructuredOutputHandling(jsonSchemaValidator, tool)).toList(); + return tools.stream() + .map(tool -> withStructuredOutputHandling(jsonSchemaValidator, duplicateStructuredContent, tool)) + .toList(); } private static McpServerFeatures.AsyncToolSpecification withStructuredOutputHandling( - JsonSchemaValidator jsonSchemaValidator, McpServerFeatures.AsyncToolSpecification toolSpecification) { + JsonSchemaValidator jsonSchemaValidator, boolean duplicateStructuredContent, + McpServerFeatures.AsyncToolSpecification toolSpecification) { if (toolSpecification.callHandler() instanceof StructuredOutputCallToolHandler) { // If the tool is already wrapped, return it as is @@ -485,8 +499,9 @@ private static McpServerFeatures.AsyncToolSpecification withStructuredOutputHand return McpServerFeatures.AsyncToolSpecification.builder() .tool(toolSpecification.tool()) - .callHandler(new StructuredOutputCallToolHandler(jsonSchemaValidator, - toolSpecification.tool().outputSchema(), toolSpecification.callHandler())) + .callHandler( + new StructuredOutputCallToolHandler(jsonSchemaValidator, toolSpecification.tool().outputSchema(), + duplicateStructuredContent, toolSpecification.callHandler())) .build(); } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java index a2333aedb..b8e888a7b 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java @@ -246,7 +246,8 @@ public McpAsyncServer build() { validateAsyncToolSchemas(jsonSchemaValidator, this.tools); return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, - features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs); + features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs, + duplicateStructuredContent); } } @@ -275,7 +276,8 @@ public McpAsyncServer build() { validateAsyncToolSchemas(jsonSchemaValidator, this.tools); return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, - features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs); + features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs, + duplicateStructuredContent); } } @@ -301,6 +303,8 @@ abstract class AsyncSpecification> { boolean validateToolInputs = true; + boolean duplicateStructuredContent = true; + /** * The Model Context Protocol (MCP) allows servers to expose tools that can be * invoked by language models. Tools enable models to interact with external @@ -440,6 +444,25 @@ public AsyncSpecification validateToolInputs(boolean validate) { return this; } + /** + * Sets whether to automatically duplicate structured content into text content + * for backwards compatibility. When enabled (the default), tools that return + * structured content will also have the serialized JSON added as a + * {@link McpSchema.TextContent} block in the {@code content} field, as + * recommended by the MCP specification. Disabling this can reduce response + * payload size when clients fully support {@code structuredContent}. + * @param duplicate true to duplicate structured content into text content + * (default), false to skip duplication + * @return This builder instance for method chaining + * @see MCP + * Structured Content + */ + public AsyncSpecification duplicateStructuredContent(boolean duplicate) { + this.duplicateStructuredContent = duplicate; + return this; + } + /** * Sets the server capabilities that will be advertised to clients during * connection initialization. Capabilities define what features the server @@ -841,7 +864,7 @@ public McpSyncServer build() { var asyncServer = new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, requestTimeout, - uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs); + uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs, duplicateStructuredContent); return new McpSyncServer(asyncServer, this.immediateExecution); } @@ -875,7 +898,8 @@ public McpSyncServer build() { var asyncServer = new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, this.requestTimeout, - this.uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs); + this.uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs, + duplicateStructuredContent); return new McpSyncServer(asyncServer, this.immediateExecution); } @@ -900,6 +924,8 @@ abstract class SyncSpecification> { boolean validateToolInputs = true; + boolean duplicateStructuredContent = true; + /** * The Model Context Protocol (MCP) allows servers to expose tools that can be * invoked by language models. Tools enable models to interact with external @@ -1043,6 +1069,25 @@ public SyncSpecification validateToolInputs(boolean validate) { return this; } + /** + * Sets whether to automatically duplicate structured content into text content + * for backwards compatibility. When enabled (the default), tools that return + * structured content will also have the serialized JSON added as a + * {@link McpSchema.TextContent} block in the {@code content} field, as + * recommended by the MCP specification. Disabling this can reduce response + * payload size when clients fully support {@code structuredContent}. + * @param duplicate true to duplicate structured content into text content + * (default), false to skip duplication + * @return This builder instance for method chaining + * @see MCP + * Structured Content + */ + public SyncSpecification duplicateStructuredContent(boolean duplicate) { + this.duplicateStructuredContent = duplicate; + return this; + } + /** * Sets the server capabilities that will be advertised to clients during * connection initialization. Capabilities define what features the server @@ -1442,6 +1487,8 @@ class StatelessAsyncSpecification { boolean validateToolInputs = true; + boolean duplicateStructuredContent = true; + /** * The Model Context Protocol (MCP) allows servers to expose tools that can be * invoked by language models. Tools enable models to interact with external @@ -1582,6 +1629,25 @@ public StatelessAsyncSpecification validateToolInputs(boolean validate) { return this; } + /** + * Sets whether to automatically duplicate structured content into text content + * for backwards compatibility. When enabled (the default), tools that return + * structured content will also have the serialized JSON added as a + * {@link McpSchema.TextContent} block in the {@code content} field, as + * recommended by the MCP specification. Disabling this can reduce response + * payload size when clients fully support {@code structuredContent}. + * @param duplicate true to duplicate structured content into text content + * (default), false to skip duplication + * @return This builder instance for method chaining + * @see MCP + * Structured Content + */ + public StatelessAsyncSpecification duplicateStructuredContent(boolean duplicate) { + this.duplicateStructuredContent = duplicate; + return this; + } + /** * Sets the server capabilities that will be advertised to clients during * connection initialization. Capabilities define what features the server @@ -1915,7 +1981,8 @@ public McpStatelessAsyncServer build() { validateStatelessAsyncToolSchemas(jsonSchemaValidator, this.tools); return new McpStatelessAsyncServer(transport, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, - features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs); + features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs, + duplicateStructuredContent); } } @@ -1942,6 +2009,8 @@ class StatelessSyncSpecification { boolean validateToolInputs = true; + boolean duplicateStructuredContent = true; + /** * The Model Context Protocol (MCP) allows servers to expose tools that can be * invoked by language models. Tools enable models to interact with external @@ -2082,6 +2151,25 @@ public StatelessSyncSpecification validateToolInputs(boolean validate) { return this; } + /** + * Sets whether to automatically duplicate structured content into text content + * for backwards compatibility. When enabled (the default), tools that return + * structured content will also have the serialized JSON added as a + * {@link McpSchema.TextContent} block in the {@code content} field, as + * recommended by the MCP specification. Disabling this can reduce response + * payload size when clients fully support {@code structuredContent}. + * @param duplicate true to duplicate structured content into text content + * (default), false to skip duplication + * @return This builder instance for method chaining + * @see MCP + * Structured Content + */ + public StatelessSyncSpecification duplicateStructuredContent(boolean duplicate) { + this.duplicateStructuredContent = duplicate; + return this; + } + /** * Sets the server capabilities that will be advertised to clients during * connection initialization. Capabilities define what features the server @@ -2433,7 +2521,7 @@ public McpStatelessSyncServer build() { var asyncServer = new McpStatelessAsyncServer(transport, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, requestTimeout, - uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs); + uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs, duplicateStructuredContent); return new McpStatelessSyncServer(asyncServer, this.immediateExecution); } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java index 3d7054cba..49f39fbe0 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java @@ -80,16 +80,19 @@ public class McpStatelessAsyncServer { private final boolean validateToolInputs; + private final boolean duplicateStructuredContent; + McpStatelessAsyncServer(McpStatelessServerTransport mcpTransport, McpJsonMapper jsonMapper, McpStatelessServerFeatures.Async features, Duration requestTimeout, McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator, - boolean validateToolInputs) { + boolean validateToolInputs, boolean duplicateStructuredContent) { this.mcpTransportProvider = mcpTransport; this.jsonMapper = jsonMapper; this.serverInfo = features.serverInfo(); this.serverCapabilities = features.serverCapabilities(); this.instructions = features.instructions(); - this.tools.addAll(withStructuredOutputHandling(jsonSchemaValidator, features.tools())); + this.tools + .addAll(withStructuredOutputHandling(jsonSchemaValidator, duplicateStructuredContent, features.tools())); this.resources.putAll(features.resources()); this.resourceTemplates.putAll(features.resourceTemplates()); this.prompts.putAll(features.prompts()); @@ -97,6 +100,7 @@ public class McpStatelessAsyncServer { this.uriTemplateManagerFactory = uriTemplateManagerFactory; this.jsonSchemaValidator = jsonSchemaValidator; this.validateToolInputs = validateToolInputs; + this.duplicateStructuredContent = duplicateStructuredContent; Map> requestHandlers = new HashMap<>(); @@ -207,17 +211,20 @@ public void close() { // --------------------------------------- private static List withStructuredOutputHandling( - JsonSchemaValidator jsonSchemaValidator, List tools) { + JsonSchemaValidator jsonSchemaValidator, boolean duplicateStructuredContent, + List tools) { if (Utils.isEmpty(tools)) { return tools; } - return tools.stream().map(tool -> withStructuredOutputHandling(jsonSchemaValidator, tool)).toList(); + return tools.stream() + .map(tool -> withStructuredOutputHandling(jsonSchemaValidator, duplicateStructuredContent, tool)) + .toList(); } private static McpStatelessServerFeatures.AsyncToolSpecification withStructuredOutputHandling( - JsonSchemaValidator jsonSchemaValidator, + JsonSchemaValidator jsonSchemaValidator, boolean duplicateStructuredContent, McpStatelessServerFeatures.AsyncToolSpecification toolSpecification) { if (toolSpecification.callHandler() instanceof StructuredOutputCallToolHandler) { @@ -232,7 +239,7 @@ private static McpStatelessServerFeatures.AsyncToolSpecification withStructuredO return new McpStatelessServerFeatures.AsyncToolSpecification(toolSpecification.tool(), new StructuredOutputCallToolHandler(jsonSchemaValidator, toolSpecification.tool().outputSchema(), - toolSpecification.callHandler())); + duplicateStructuredContent, toolSpecification.callHandler())); } private static class StructuredOutputCallToolHandler @@ -244,8 +251,10 @@ private static class StructuredOutputCallToolHandler private final Map outputSchema; + private final boolean duplicateStructuredContent; + public StructuredOutputCallToolHandler(JsonSchemaValidator jsonSchemaValidator, - Map outputSchema, + Map outputSchema, boolean duplicateStructuredContent, BiFunction> delegateHandler) { Assert.notNull(jsonSchemaValidator, "JsonSchemaValidator must not be null"); @@ -253,6 +262,7 @@ public StructuredOutputCallToolHandler(JsonSchemaValidator jsonSchemaValidator, this.delegateHandler = delegateHandler; this.outputSchema = outputSchema; + this.duplicateStructuredContent = duplicateStructuredContent; this.jsonSchemaValidator = jsonSchemaValidator; } @@ -300,7 +310,7 @@ public Mono apply(McpTransportContext transportContext, McpSchem .build(); } - if (Utils.isEmpty(result.content())) { + if (this.duplicateStructuredContent && Utils.isEmpty(result.content())) { // For backwards compatibility, a tool that returns structured // content SHOULD also return functionally equivalent unstructured // content. (For example, serialized JSON can be returned in a @@ -348,7 +358,8 @@ public Mono addTool(McpStatelessServerFeatures.AsyncToolSpecification tool return Mono.error(e); } - var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification); + var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, + this.duplicateStructuredContent, toolSpecification); return Mono.defer(() -> { // Remove tools with duplicate tool names first